web-dev-qa-db-fra.com

Une interdiction «longue» a-t-elle un sens?

Dans le monde multiplateforme C++ (ou C) d'aujourd'hui, nous avons :

Data model  | short |   int |   long | long long | pointers/size_t  | Sample operating systems
... 
LLP64/IL32P64   16      32      32     64           64                Microsoft Windows (x86-64 and IA-64)
LP64/I32LP64    16      32      64     64           64                Most Unix and Unix-like systems, e.g. Solaris, Linux, BSD, and OS X; z/OS
...

Ce que cela signifie aujourd'hui, c'est que pour tout entier "commun" (signé), int suffira et peut éventuellement encore être utilisé comme type d'entier par défaut lors de l'écriture du code d'application C++. Il aura également - à des fins pratiques actuelles - aura une taille cohérente sur toutes les plateformes.

Si un cas d'utilisation nécessite au moins 64 bits, nous pouvons aujourd'hui utiliser long long, mais peut-être en utilisant l'un des types spécifiant le nombre ou __int64type pourrait avoir plus de sens.

Cela laisse long au milieu, et nous envisageons d'interdire carrément l'utilisation de long de notre code d'application .

Est-ce que cela aurait du sens , ou existe-t-il un cas d'utilisation de long dans du code C++ (ou C) moderne qui doit exécuter plusieurs plateformes ? (la plate-forme étant un ordinateur de bureau, des appareils mobiles, mais pas des choses comme des microcontrôleurs, des DSP, etc.)


Liens de fond potentiellement intéressants:

109
Martin Ba

La seule raison pour laquelle j'utiliserais long aujourd'hui, c'est lorsque j'appelle ou implémente une interface externe qui l'utilise.

Comme vous le dites dans votre article, short et int ont des caractéristiques raisonnablement stables sur toutes les principales plates-formes de bureau/serveur/mobiles aujourd'hui et je ne vois aucune raison pour que cela change dans un avenir prévisible. Je vois donc peu de raisons de les éviter en général.

long d'autre part est un gâchis. Sur tous les systèmes 32 bits, je sais qu'il avait les caractéristiques suivantes.

  1. Il faisait exactement 32 bits.
  2. C'était la même taille qu'une adresse mémoire.
  3. Il avait la même taille que la plus grande unité de données qui pouvait être conservée dans un registre normal et travailler avec une seule instruction.

De grandes quantités de code ont été écrites sur la base d'une ou plusieurs de ces caractéristiques. Cependant, avec le passage au 64 bits, il n'a pas été possible de les conserver tous. Les plates-formes de type Unix ont opté pour LP64 qui a conservé les caractéristiques 2 et 3 au prix de la caractéristique 1. Win64 a opté pour LLP64 qui a préservé la caractéristique 1 au prix des caractéristiques 2 et 3. Le résultat est que vous ne pouvez plus compter sur aucune de ces caractéristiques. et que l'OMI laisse peu de raisons d'utiliser long.

Si vous voulez un type exactement de 32 bits, vous devez utiliser int32_t.

Si vous voulez un type de la même taille qu'un pointeur, vous devez utiliser intptr_t (ou mieux uintptr_t).

Si vous voulez un type qui soit le plus gros élément sur lequel travailler dans un seul registre/instruction, malheureusement je ne pense pas que la norme en fournisse un. size_t devrait avoir raison sur la plupart des plates-formes courantes, mais ce ne serait pas le cas sur x32 .


P.S.

Je ne m'embêterais pas avec les types "rapides" ou "les moins". Les "moindres" types n'ont d'importance que si vous vous souciez de la portabilité pour vraiment obscurcir les architectures où CHAR_BIT != 8. La taille des types "rapides" dans la pratique semble assez arbitraire. Linux semble leur donner au moins la même taille que le pointeur, ce qui est idiot sur les plates-formes 64 bits avec un support rapide 32 bits comme x86-64 et arm64. IIRC iOS les rend aussi petits que possible. Je ne sais pas ce que font les autres systèmes.


P.P.S

Une raison d'utiliser unsigned long (mais pas simple long) est parce qu'il est garanti d'avoir un comportement modulo. Malheureusement, en raison des règles de promotion ratées de C, les types non signés plus petits que int n'ont pas de comportement modulo.

Sur toutes les principales plateformes aujourd'hui uint32_t est de la même taille ou plus grande que int et a donc un comportement modulo. Cependant, il y a eu historiquement et il pourrait y avoir théoriquement dans les futures plates-formes où int est 64 bits et donc uint32_t n'a pas de comportement modulo.

Personnellement, je dirais qu'il vaut mieux s'habituer à forcer le comportement modulo en utilisant "1u *" ou "0u +" au début de vos équations car cela fonctionnera pour n'importe quelle taille de type non signé.

18
Peter Green

Comme vous le mentionnez dans votre question, un logiciel moderne est une question d'interopérabilité entre plates-formes et systèmes sur Internet. Les normes C et C++ donnent des plages pour les tailles de type entier, pas des tailles spécifiques (contrairement aux langages comme Java et C #).

Pour vous assurer que votre logiciel compilé sur différentes plates-formes fonctionne avec les mêmes données de la même manière et pour vous assurer que d'autres logiciels peuvent interagir avec votre logiciel en utilisant les mêmes tailles , vous devez utiliser des entiers de taille fixe.

Entrer <cstdint> qui fournit exactement cela et est un en-tête standard que toutes les plates-formes de compilation et de bibliothèque standard doivent fournir. Remarque: cet en-tête n'était requis que depuis C++ 11, mais de nombreuses implémentations de bibliothèques plus anciennes le fournissaient quand même.

Vous voulez un entier 64 bits non signé? Utilisation uint64_t. Entier 32 bits signé? Utilisation int32_t. Bien que les types de l'en-tête soient facultatifs, les plates-formes modernes doivent prendre en charge tous les types définis dans cet en-tête.

Parfois, une largeur de bit spécifique est nécessaire, par exemple, dans une structure de données utilisée pour communiquer avec d'autres systèmes. D'autres fois, ce n'est pas le cas. Pour les situations moins strictes, <cstdint> fournit des types d'une largeur minimale.

Il existe le moins variantes: int_leastXX_t sera un type entier de XX bits minimum. Il utilisera le plus petit type qui fournit XX bits, mais le type peut être supérieur au nombre de bits spécifié. En pratique, ce sont généralement les mêmes que les types décrits ci-dessus qui donnent le nombre exact de bits.

Il existe également des variantes rapides : int_fastXX_t fait au moins XX bits, mais doit utiliser un type qui fonctionne rapidement sur une plate-forme particulière. La définition de "rapide" dans ce contexte n'est pas spécifiée. Cependant, dans la pratique, cela signifie généralement qu'un type inférieur à la taille de registre d'un processeur peut être alias à un type de taille de registre du processeur. Par exemple, l'en-tête de Visual C++ 2015 spécifie que int_fast16_t est un entier 32 bits car l'arithmétique 32 bits est globalement plus rapide sur x86 que l'arithmétique 16 bits.

Tout cela est important car vous devriez pouvoir utiliser des types qui peuvent contenir les résultats des calculs effectués par votre programme quelle que soit la plate-forme. Si un programme produit des résultats corrects sur une plate-forme mais des résultats incorrects sur une autre en raison de différences de dépassement d'entier, c'est mauvais. En utilisant les types d'entiers standard, vous garantissez que les résultats sur différentes plates-formes seront les mêmes en ce qui concerne la taille des entiers utilisés (bien sûr il pourrait y avoir autres différences entre plates-formes que la largeur entière).

Donc oui, long devrait être banni du code C++ moderne. De même, int, short et long long.

203
user22815

Non, interdire les types entiers intégrés serait absurde. Cependant, ils ne devraient pas non plus être maltraités.

Si vous avez besoin d'un entier qui est exactement N bits de large, utilisez std::intN_t (ou std::uintN_t si vous avez besoin d'une version unsigned). Penser à int comme un entier 32 bits et long long comme un entier 64 bits est tout simplement faux. Cela peut arriver comme ça sur vos plates-formes actuelles, mais cela dépend d'un comportement défini par l'implémentation.

L'utilisation de types entiers à largeur fixe est également utile pour interagir avec d'autres technologies. Par exemple, si certaines parties de votre application sont écrites en Java et d'autres en C++, vous voudrez probablement faire correspondre les types d'entiers afin d'obtenir des résultats cohérents. (Gardez toujours à l'esprit que le débordement dans Java a une sémantique bien définie tandis que signed le débordement en C++ est un comportement indéfini donc la cohérence est un objectif élevé.) Ils seront également inestimables lors de l'échange de données entre différents hôtes de calcul.

Si vous n'avez pas besoin exactement N bits, mais juste un type qui est assez large, pensez à utiliser std::int_leastN_t (optimisé pour l'espace) ou std::int_fastN_t (optimisé pour la vitesse). Encore une fois, les deux familles ont également unsigned homologues.

Alors, quand utiliser les types intégrés? Eh bien, puisque la norme ne spécifie pas précisément leur largeur, utilisez-les lorsque vous ne pas vous souciez de la largeur réelle des bits mais d'autres caractéristiques.

Un char est le plus petit entier adressable par le matériel. Le langage vous oblige en fait à l'utiliser pour aliaser la mémoire arbitraire. C'est également le seul type viable pour représenter des chaînes de caractères (étroites).

Un int sera généralement le type le plus rapide que la machine peut gérer. Il sera suffisamment large pour pouvoir être chargé et stocké avec une seule instruction (sans avoir à masquer ou décaler les bits) et suffisamment étroit pour pouvoir être utilisé avec les instructions matérielles (les plus) efficaces. Par conséquent, int est un choix parfait pour transmettre des données et faire de l'arithmétique lorsque le débordement n'est pas un problème. Par exemple, le type d'énumération sous-jacent par défaut est int. Ne le changez pas en un entier 32 bits simplement parce que vous le pouvez. De plus, si vous avez une valeur qui ne peut être que –1, 0 et 1, un int est un choix parfait, sauf si vous allez en stocker d'énormes tableaux, auquel cas vous souhaiterez peut-être utiliser un type de données plus compact au prix d'avoir à payer un prix plus élevé pour accéder aux éléments individuels. Une mise en cache plus efficace sera probablement payante pour ces derniers. De nombreuses fonctions du système d'exploitation sont également définies en termes de int. Il serait stupide de convertir leurs arguments et leurs résultats d'avant en arrière. Tout cela pourrait éventuellement être de introduire des erreurs de débordement.

long sera généralement le type le plus large pouvant être géré avec des instructions sur une seule machine. Cela fait surtout unsigned long très attrayant pour traiter des données brutes et toutes sortes de trucs de manipulation de bits. Par exemple, je m'attendrais à voir unsigned long dans l'implémentation d'un vecteur de bits. Si le code est écrit avec soin, peu importe la largeur réelle du type (car le code s'adaptera automatiquement). Sur les plates-formes où la machine-Word native est de 32 bits, avoir le tableau de support du vecteur de bits être un tableau de unsigned entiers 32 bits est le plus souhaitable car il serait idiot d'utiliser un type 64 bits qui a à charger via des instructions coûteuses uniquement pour décaler et masquer à nouveau les bits inutiles. D'un autre côté, si la taille Word native de la plate-forme est de 64 bits, je veux un tableau de ce type car cela signifie que les opérations comme "trouver le premier ensemble" peuvent s'exécuter jusqu'à deux fois plus rapidement. Ainsi, le "problème" du type de données long que vous décrivez, que sa taille varie d'une plateforme à l'autre, est en fait un fonctionnalité qui peut être utilisé à bon escient. Cela ne devient un problème que si vous considérez les types intégrés comme des types d'une certaine largeur de bits, ce qu'ils ne sont tout simplement pas.

char, int et long sont des types très utiles comme décrit ci-dessus. short et long long ne sont pas aussi utiles car leur sémantique est beaucoup moins claire.

38
5gon12eder

Une autre réponse détaille déjà les types cstdint et les variations moins connues de ceux-ci.

Je voudrais ajouter à cela:

utiliser des noms de type spécifiques au domaine

Autrement dit, ne déclarez pas vos paramètres et variables comme uint32_t (certainement pas long!), mais des noms tels que channel_id_type, room_count_type etc.

sur les bibliothèques

Les bibliothèques tierces qui utilisent long ou autres peuvent être ennuyeuses, surtout si elles sont utilisées comme références ou pointeurs vers celles-ci.

La chose meilleure est de faire des wrappers.

En général, ma stratégie consiste à créer un ensemble de fonctions de type cast qui seront utilisées. Ils sont surchargés pour accepter uniquement les types qui correspondent exactement aux types correspondants, ainsi que les variations de pointeur, etc. dont vous avez besoin. Ils sont définis spécifiques au système d'exploitation/compilateur/paramètres. Cela vous permet de supprimer les avertissements tout en vous assurant que seules les "bonnes" conversions sont utilisées.

channel_id_type cid_out;
...
SomeLibFoo (same_thing_really<int*>(&cid_out));

En particulier, avec différents types primitifs produisant 32 bits, votre choix de la façon dont int32_t est défini peut ne pas correspondre à l'appel de bibliothèque (par exemple, int vs long sous Windows).

La fonction de type cast documents le conflit, prévoit une vérification à la compilation du résultat correspondant au paramètre de la fonction et supprime tout avertissement ou erreur si et seulement si le type réel correspond à la taille réelle impliquée. Autrement dit, il est surchargé et défini si je passe (sous Windows) un int* ou un long* et donne une erreur de compilation sinon.

Donc, si la bibliothèque est mise à jour ou si quelqu'un change ce que channel_id_type est, cela continue d'être vérifié.

6
JDługosz