186 votes

Est-ce qu'il y a encore une raison d'utiliser `int` dans du code C++?

De nombreux guides de style, tels que celui de Google, recommandent d'utiliser int comme entier par défaut lors de l'indexation de tableaux par exemple. Avec l'avènement des plateformes 64 bits où la plupart du temps un int est seulement 32 bits, ce qui n'est pas la largeur naturelle de la plateforme. En conséquence, je ne vois aucune raison, à part la simplicité, de conserver ce choix. Nous voyons clairement cela en compilant le code suivant:

double get(const double* p, int k) {
  return p[k];
}

qui est compilé en

movslq %esi, %rsi
vmovsd (%rdi,%rsi,8), %xmm0
ret

où la première instruction promeut l'entier de 32 bits en un entier de 64 bits.

Si le code est transformé en

double get(const double* p, std::ptrdiff_t k) {
  return p[k];
}

l'assemblage généré est maintenant

vmovsd (%rdi,%rsi,8), %xmm0
ret

ce qui montre clairement que le CPU se sent plus à l'aise avec std::ptrdiff_t qu'avec un int. De nombreux utilisateurs de C++ sont passés à std::size_t, mais je ne veux pas utiliser d'entiers non signés sauf si j'ai vraiment besoin d'un comportement de modulo 2^n.

Dans la plupart des cas, utiliser int n'a pas d'impact sur les performances car le comportement indéfini ou les débordements d'entiers signés permettent au compilateur de promouvoir internalement n'importe quel int en std::ptrdiff_t dans les boucles traitant des indices, mais nous voyons clairement à partir de ce qui précède que le compilateur ne se sent pas à l'aise avec int. De plus, utiliser std::ptrdiff_t sur une plateforme 64 bits rendrait moins probable l'apparition de débordements car je vois de plus en plus de personnes piégées par les débordements d'int lorsqu'elles doivent traiter des entiers plus grands que 2^31 - 1 qui deviennent vraiment courants de nos jours.

De ce que j'ai vu, la seule chose qui distingue int semble être le fait que les littéraux tels que 5 sont des int, mais je ne vois pas où cela pourrait poser problème si nous passons à std::ptrdiff_t comme entier par défaut.

Je suis sur le point de faire de std::ptrdiff_t l'entier standard de facto pour tout le code écrit dans ma petite entreprise. Y a-t-il une raison pour laquelle ce pourrait être un mauvais choix?

PS: _Je suis d'accord sur le fait que le nom std::ptrdiff_t est laid, c'est la raison pour laquelle je l'ai typedefié en il::int_t qui semble un peu mieux._

PS: Comme je sais que beaucoup de gens me recommanderont d'utiliser std::size_t comme entier par défaut, je tiens vraiment à préciser que je ne veux pas utiliser un entier non signé comme entier par défaut. L'utilisation de std::size_t comme entier par défaut dans la STL a été une erreur, comme l'ont reconnu Bjarne Stroustrup et le comité de normalisation dans la vidéo Interactive Panel: Ask Us Anything à 42:38 et 1:02:50.

PS: _En termes de performances, sur n'importe quelle plateforme 64 bits que je connais, +, - et * sont compilés de la même manière pour int et std::ptrdiff_t. Il n'y a donc aucune différence de vitesse. Si vous divisez par une constante au moment de la compilation, la vitesse est la même. C'est seulement lorsque vous divisez a/b sans savoir quoi que ce soit sur b que l'utilisation d'un entier de 32 bits sur une plateforme 64 bits vous donne un léger avantage en termes de performances. Mais ce cas est si rare que je ne vois pas pourquoi je devrais m'éloigner de std::ptrdiff_t. Lorsque nous traitons du code vectorisé, là il y a une différence claire, et plus petit est mieux, mais c'est une autre histoire, et il n'y aurait aucune raison de rester avec int. Dans ces cas, je recommanderais d'utiliser les types de taille fixe de C++._

34 votes

"De nombreux guides de styles, tels que celui de Google, recommandent d'utiliser int comme entier par défaut, par exemple pour l'indexation des tableaux" - citation nécessaire. Vous devriez toujours utiliser size_t ou size_type (STL).

30 votes

"qui est compilé en" - Avec quels drapeaux et par quel compilateur?

33 votes

En utilisant int pour indexer dans les tableaux est simplement incorrect car il n'y a aucune garantie que int soit assez grand pour couvrir tous les indices possibles dans un tableau. std::size_t est le bon type pour cela.

106voto

Robert Andrzejuk Points 3174

Il y avait une discussion sur les directives du C++ Core Guidelines quant à l'utilisation de :

https://github.com/isocpp/CppCoreGuidelines/pull/1115

Herb Sutter a écrit que gsl::index sera ajouté (peut-être dans le futur std::index), qui sera défini comme ptrdiff_t.

hsutter a commenté le 26 déc. 2017 •

(Merci à de nombreux experts de la WG21 pour leurs commentaires et leurs retours sur cette note.)

Ajoutez le typedef suivant à GSL

namespace gsl { using index = ptrdiff_t; }

et recommandez gsl::index pour tous les indexes/sous-indices/tailles de conteneurs.

Rationnel

Les directives recommandent l'utilisation d'un type signé pour les sous-indices/index. Voir ES.100 à ES.107. C++ utilise déjà des entiers signés pour les sous-indices des tableaux.

Nous voulons être en mesure d'enseigner aux gens à écrire du "nouveau code moderne propre" qui soit simple, naturel, sans avertissement à des niveaux d'avertissement élevés et qui ne nous oblige pas à écrire une astérisque de "problème" à propos d'un code simple.

Si nous n'avions pas un mot court adoptable comme index qui soit compétitif avec int et auto, les gens continueraient d'utiliser int et auto et rencontreraient des bugs. Par exemple, ils écriraient for(int i=0; i ou ``for(auto i=0; i qui ont des bugs de taille 32 bits sur des plateformes largement utilisées, et `for(auto i=v.size()-1; i>=0; ++i)` qui ne fonctionne tout simplement pas. Je ne pense pas que nous puissions enseigner `for(ptrdiff_t i = ...` avec un visage sérieux, ou que les gens l'accepteraient.``

`

Si nous avions un type arithmétique saturant, nous pourrions l'utiliser. Sinon, la meilleure option est ptrdiff_t qui a presque tous les avantages d'un type non signé arithmétique saturant, à l'exception que ptrdiff_t rend encore le style de boucle omniprésente for(ptrdiff_t i=0; i émet des inadéquations signées/non signées sur ``i (et pareillement pour `i!=v.size()`) pour les conteneurs STL d'aujourd'hui. (Si un futur STL change son size_type pour être signé, même ce dernier inconvénient disparaît.)``

`

Cependant, il serait désespéré (et embarrassant) d'essayer d'enseigner aux gens à écrire régulièrement for (ptrdiff_t i = ... ; ... ; ...). (Même les directives l'utilisent actuellement à un seul endroit, et c'est un exemple "mauvais" qui n'a rien à voir avec l'indexation.)

Par conséquent, nous devrions fournir gsl::index (qui pourrait ensuite être proposé pour considération comme std::index) comme un typedef pour ptrdiff_t, afin que nous puissions espérer (et sans embarrasser) enseigner aux gens à écrire régulièrement (index i = ... ; ... ; ...).

Pourquoi ne pas simplement dire aux gens d'écrire ptrdiff_t? Parce que nous croyons que ce serait embarrassant d'indiquer aux gens que c'est ce qu'ils doivent faire en C++, et même s'ils le faisaient, les gens ne le feraient pas. Écrire ptrdiff_t est trop laid et non adoptable par rapport à auto et int. Le but d'ajouter le nom index est de rendre aussi facile et attrayant que possible l'utilisation d'un type signé correctement dimensionné.

`` ``` ```` `````

`

Modifier : Plus de raisons de Herb Sutter

Est-ce que ptrdiff_t est assez grand? Oui. Les conteneurs standard sont déjà tenus de ne pas avoir plus d'éléments qu'il est possible de représenter par ptrdiff_t, car soustraire deux itérateurs doit correspondre à un difference_type.

Mais ptrdiff_t est-il vraiment assez grand, si j'ai un tableau intégré de char ou de byte qui est plus grand que la moitié de la taille de l'espace d'adressage mémoire et donc a plus d'éléments qu'il ne peut être représenté dans un ptrdiff_t? Oui. C++ utilise déjà des entiers signés pour les sous-indices des tableaux. Utilisez donc index comme option par défaut pour la grande majorité des utilisations, y compris tous les tableaux intégrés. (Si vous rencontrez le cas extrêmement rare d'un tableau, ou d'un type similaire à un tableau, qui est plus grand que la moitié de l'espace d'adressage et dont les éléments sont de sizeof(1), et que vous êtes attentif pour éviter les problèmes de troncature, utilisez un size_t pour les indexes dans ce conteneur très spécial uniquement. Ces bêtes sont très rares en pratique, et lorsqu'elles se présentent, elles ne seront souvent pas indexées directement par le code utilisateur. Par exemple, elles apparaissent généralement dans un gestionnaire de m&eacu

`` ```

0 votes

Merci pour l'info. Je n'ai probablement pas regardé le GSL et j'ai manqué ce point.

5 votes

En réponse au texte cité; l'utilisation d'un index signé pose évidemment le problème de provoquer un dépassement d'entier lors de l'accès à un conteneur dont la taille dépasse SIZE_MAX/2. J'espère qu'il y a aussi d'autres changements pour résoudre ce problème (par exemple, en définissant la taille maximale d'un objet à réellement être SIZE_MAX/2 plutôt que SIZE_MAX).

0 votes

@M.M pour(auto i=v.size()-1; i!=(~(size_t)(0)); --i) est moche, mais fonctionne principalement. Le problème avec ptrdiff_t est qu'idéalement vous voulez qu'il soit 1 bit plus large que size_t, mais c'est impossible, donc il ne couvre que la moitié de l'espace d'adressage. Si vous divisez SIZE_MAX pour qu'il soit couvert par un int, alors il n'y a pas beaucoup d'intérêt à avoir size_t non signé et ptrdiff_t signé en même temps.

37voto

little_birdie Points 2824

Je viens de cette perspective en tant que vieux (avant le C++)... Il était entendu à l'époque que int était le mot natif de la plateforme et offrait probablement les meilleures performances.

Si vous aviez besoin de quelque chose de plus grand, alors vous l'utiliseriez et paieriez le prix en termes de performances. Si vous aviez besoin de quelque chose de plus petit (mémoire limitée, ou besoin spécifique d'une taille fixe), même chose... sinon utilisez int. Et oui, si votre valeur était dans la plage où int sur une plateforme cible pouvait l'accommoder et int sur une autre plateforme cible ne le pouvait pas... alors nous avions nos définitions spécifiques de taille au moment de la compilation (avant qu'elles ne soient normalisées, nous fabriquions les nôtres).

Mais maintenant, de nos jours, les processeurs et les compilateurs sont beaucoup plus sophistiqués et ces règles ne s'appliquent pas aussi facilement. Il est également plus difficile de prédire l'impact de performance de votre choix sur une future plateforme ou compilateur inconnus... Comment pouvons-nous vraiment savoir si uint64_t par exemple offrira de meilleures ou de pires performances que uint32_t sur une plateforme future particulière ? À moins d'être un expert en processeurs/compilateurs, vous ne le savez pas...

Alors... peut-être que c'est démodé, mais à moins que je n'écrive du code pour un environnement contraint comme Arduino, etc. j'utilise toujours int pour des valeurs à usage général que je sais rester dans la taille de int sur toutes les cibles raisonnables pour l'application que j'écris. Et le compilateur gère le reste... De nos jours, cela signifie généralement 32 bits signés. Même en supposant que 16 bits est la taille minimale d'entier, cela couvre la plupart des cas d'utilisation... et les cas d'utilisation pour des nombres plus grands que cela sont facilement identifiés et traités avec des types appropriés.

10 votes

Plus précisément, ce n'est tout simplement plus vrai que int est le mot natif de la plateforme. Avec l'adoption généralisée du 64 bits, les considérations héritées ont laissé int derrière. Bien sûr, c'est pourquoi des types comme size_t existent en premier lieu - car votre système sait mieux.

0 votes

Je trouve cette approche de codage dérangeante. "Ça marche peut-être et j'ai l'habitude, donc je vais l'utiliser". Vous ne voyez probablement pas souvent des tableaux de 2*31 + 1 éléments, mais si c'est le cas, avec 32 Go de RAM ou plus, c'est facile. De plus, le code utilisant int est sous-spécifié. En l'utilisant, vous dites "Je connais toutes les plates-formes sur lesquelles ce code sera jamais exécuté et la taille d'int sur ces plates-formes et ça correspond à son utilisation". À mon avis, cette manière de penser est la raison derrière les problèmes de portabilité pour certains projets majeurs.

10 votes

Présupposément, un tableau de 2*31+1 éléments n'apparaît pas simplement de manière aléatoire dans le code un jour sans que l'on en soit conscient. Il s'agit d'une décision de conception délibérée, auquel cas, utilisez un type adéquat. Tapez-vous également explicitement les valeurs d'index en unint8_t si c'est un petit tableau?

20voto

Eyal K. Points 625

La plupart des programmes ne dépendent pas uniquement de quelques cycles CPU, et int est très facile à écrire. Cependant, si vous êtes sensible aux performances, je vous suggère d'utiliser les types entiers de largeur fixe définis dans , tels que int32_t ou uint64_t. Ces types ont l'avantage d'être très clairs quant à leur comportement attendu en ce qui concerne le signe ou l'absence de signe, ainsi que leur taille en mémoire. Cet en-tête inclut également les variantes rapides telles que int_fast32_t, qui sont au moins de la taille indiquée, mais peuvent être plus grandes si cela améliore les performances.

10 votes

En utilisant des types d'entiers 64 bits fixes pour les indices nuit aux performances sur les systèmes 32 bits. size_t ou ptrdiff_t sont beaucoup mieux à cet égard.

2 votes

Aussi, les types de largeur exacte sont optionnels et ne doivent pas nécessairement exister, donc du moins du point de vue de la portabilité théorique de votre code, je ne pense pas qu'il soit bon de les utiliser sans une exigence de largeur fixe spécifique.

3 votes

Tu mélanges la taille du code et l'efficacité. Les types de taille fixe comme int32_t sont en général, significativement moins efficaces, car en imposant une taille exacte, ils contraignent le compilateur à ne pas utiliser la taille qui pourrait être plus naturelle ou plus efficace sur le plan computationnel.

16voto

Uprooted Points 777

Pas de raison formelle d'utiliser int. Ça ne correspond à rien de sain selon les normes. Pour les indices, vous voulez presque toujours utiliser un entier signé de la taille d'un pointeur.

Cela dit, taper int donne l'impression que vous venez de dire bonjour à Ritchie et taper std::ptrdiff_t donne l'impression que Stroustrup vient de vous donner un coup de pied aux fesses. Les codeurs sont aussi des gens, ne leur apportez pas trop laideur dans leur vie. Je préférerais utiliser long ou un typedef facilement tapé comme index au lieu de std::ptrdiff_t.

2 votes

Les membres du comité C++ s'accordent à dire que ptrdiff_t est moche. Ils proposent donc que "index" soit utilisé.

0 votes

Soit je n'ai pas lu ta réponse en entier soit tu l'as prolongée, je suppose qu'elle couvre entièrement la question maintenant. J'ai toujours pensé que les gens sous-estiment le besoin d'élégance dans le codage, mais apparemment le comité ne le fait pas.

4 votes

"Pour les indices, vous voulez presque toujours un type long de mot machine signé." Qui correspond à la définition de "int"!

12voto

Damon Points 26437

C'est un peu basé sur l'opinion, mais hélas, la question semble un peu le demander aussi.

Tout d'abord, vous parlez des entiers et indices comme s'ils étaient la même chose, ce qui n'est pas le cas. Pour toute chose de ce genre "entier de quelque sorte, pas certain de la taille", simplement utiliser int est bien sûr, la plupart du temps, toujours approprié. Cela fonctionne bien la plupart du temps, pour la plupart des applications, et le compilateur est à l'aise avec cela. Par défaut, c'est bien.

Pour les indices de tableau, c'est une autre histoire.

Il existe à ce jour une seule chose formellement correcte, et c'est std::size_t. À l'avenir, il pourrait y avoir une std::index_t qui rende l'intention plus claire au niveau de la source, mais jusqu'à présent, ça n'existe pas.
std::ptrdiff_t en tant qu'index "fonctionne" mais est tout aussi incorrect que int car il permet des indices négatifs.
Oui, cela fonctionne ce que M. Sutter juge correct, mais je suis en désaccord. Oui, au niveau de l'instruction en langage d'assemblage, cela est bien supporté, mais je m'oppose toujours. La norme dit:

8.3.4/6: E1[E2] est identique à *((E1)+(E2)) [...] En raison des règles de conversion qui s'appliquent à +, si E1 est un tableau et E2 un entier, alors E1[E2] fait référence au E2-ième membre de E1.
5.7/5: [...] Si l'opérande pointeur et le résultat pointent vers des éléments du même objet tableau, ou un au-delà du dernier élément de l'objet tableau [...] sinon, le comportement est indéfini.

Un abonnement de tableau fait référence au E2-ième membre de E1. Il n'y a pas de tel élément négatif d'un tableau. Mais plus important encore, l'arithmétique des pointeurs avec une expression additive négative engendre un comportement indéfini.

En d'autres termes: les indices signés de quelque taille que ce soit sont un mauvais choix. Les indices sont non signés. Oui, les indices signés fonctionnent, mais ils sont toujours incorrects.

Maintenant, bien que size_t soit par définition le choix correct (un type entier non signé assez grand pour contenir la taille de n'importe quel objet), il peut être discutable de savoir s'il est vraiment un bon choix pour le cas moyen, ou comme une valeur par défaut.

Soyez honnête, quand était la dernière fois que vous avez créé un tableau avec 1019 éléments?

Je suis personnellement en train d'utiliser unsigned int comme valeur par défaut car les 4 milliards d'éléments que cela permet sont largement suffisants pour (presque) chaque application, et cela pousse déjà l'ordinateur moyen de l'utilisateur plutôt proche de sa limite (si seulement s'abonner à un tableau d'entiers, celà suppose 16Go de mémoire contiguë allouée). Je considère personnellement le fait de passer en indices de 64 bits par défaut comme ridicule.

Si vous programmez une base de données relationnelle ou un système de fichiers, alors oui, vous aurez besoin d'indices de 64 bits. Mais pour le programme "normal" moyen, les indices de 32 bits sont amplement suffisants, et ils ne consomment que la moitié du stockage.

Lorsque vous gardez considérablement plus qu'une poignée d'indices, et si je peux me le permettre (parce que les tableaux ne sont pas plus larges que 64k éléments), je descends même à uint16_t. Non, je ne plaisante pas.

Le stockage est-il vraiment un problème? Est-il ridicule d'être avare de deux ou quatre octets économisés, n'est-ce pas! Eh bien, non...

La taille peut poser un problème pour les pointeurs, donc bien sûr elle peut également poser un problème pour les indices. L'ABI x32 n'existe pas sans raison. Vous ne remarquerez pas la surcharge de grands indices inutilement si vous n'en avez que quelques-uns au total (tout comme les pointeurs, ils seront de toute façon dans des registres, personne ne remarquera s'ils font 4 ou 8 octets de taille).

Mais pensez par exemple à une carte de fente où vous stockez un index pour chaque élément (selon l'implémentation, deux indices par élément). Oh diable, ça fait vraiment une différence si vous touchez chaque fois L2, ou si vous avez une absence de cache à chaque accès! Plus grand n'est pas toujours mieux.

En fin de compte, vous devez vous demander ce que vous payez, et ce que vous obtenez en retour. Avec cela à l'esprit, ma recommandation de style serait la suivante:

Si cela ne vous coûte "rien" car vous n'avez par exemple qu'un pointeur et quelques indices à conserver, utilisez simplement ce qui est formellement correct (ce serait size_t). Formellement correct est bon, correct fonctionne toujours, c'est lisible et intelligible, et correct est... jamais faux.

Si, cependant, cela vous coûte (vous avez peut-être plusieurs centaines ou milliers ou dizaines de milliers d'indices), et ce que vous obtenez en retour ne vaut rien (par exemple, vous ne pouvez même pas stocker 220 éléments, donc que vous pourriez vous abonner à 232 ou 264 ne fait aucune différence), vous devriez réfléchir à deux fois avant d'être trop gaspilleur.

3 votes

Je suis désolé de ne pas être d'accord avec vous sur le fait que les indices devraient être des entiers non signés. Comme de nombreuses personnes de la communauté C++, vous avez tort. Non seulement Herb Sutter mais aussi Bjarne Stroustrup et Chandler Carruth sont d'accord sur ce point et pensent que l'interface STL a fait le mauvais choix. Oui, les indices sont des entiers non négatifs. Et alors ? La division entière par 0 implique un comportement indéfini et personne n'a ressenti le besoin de créer un type qui n'inclut pas 0. En outre, si p est un pointeur et q = p + n, il s'avère que q - p est, selon la norme, un ptrdiff_t. Il semble naturel que n soit de ce type.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X