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'essor des plates-formes 64 bits où la plupart du temps un int
ne représente que 32 bits, ce qui n'est pas la largeur naturelle de la plate-forme. En conséquence, je ne vois aucune raison, si ce n’est simple, 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é dans
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 processeur est 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 souhaite pas utiliser d'entiers non signés, sauf si j'ai vraiment besoin du comportement modulo 2^n
.
Dans la plupart des cas, l'utilisation de int
ne nuit pas aux performances, car le comportement non défini ou les débordements d'entiers signés permettent au compilateur de promouvoir en interne tout int
en std::ptrdiff_t
in boucles qui traitent d'index, voyez clairement de ce qui précède que le compilateur ne se sent pas chez lui avec int
. De plus, en utilisant std::ptrdiff_t
sur une plate-forme 64 bits, les débordements seraient moins probables, car je vois de plus en plus de personnes se faire piéger par int
débordements lorsqu'elles doivent traiter des entiers supérieurs à 2^31 - 1
qui deviennent vraiment communs de nos jours.
D'après ce que j'ai vu, la seule chose qui distingue int
semble être le fait que des littéraux tels que 5
sont int
, mais je ne vois pas en quoi cela pourrait en causer problème si nous passons à std::ptrdiff_t
comme un entier par défaut.
Je suis sur le point de faire de std::ptrdiff_t
le nombre entier standard de facto pour tout le code écrit dans ma petite entreprise. Y a-t-il une raison pour laquelle cela pourrait être un mauvais choix?
PS: Je suis d’accord avec le fait que le nom std::ptrdiff_t
est laid, c’est la raison pour laquelle je l’ai tapé dans il::int_t
qui a l’air un peu meilleur.
PS: sachant que beaucoup de gens me recommanderont d'utiliser std::size_t
comme entier par défaut, je tiens vraiment à préciser que je ne souhaite 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'a reconnu Bjarne Stroustrup et le comité standard de la vidéo Panel interactif: Demandez-nous n'importe quoi. aux heures 42:38 et 1:02:50.
PS: En termes de performances, sur toute plate-forme 64 bits que je connaisse, +
, -
et *
sont compilés de la même manière pour int
et std::ptrdiff_t
. Donc, il n'y a pas de différence de vitesse. Si vous divisez par une constante de compilation, la vitesse est la même. Ce n'est que lorsque vous divisez a/b
lorsque vous ne connaissez rien à b
que l'utilisation d'un entier 32 bits sur une plate-forme 64 bits vous donne un léger avantage en termes de performances. Mais cette affaire est si rare que je ne vois pas le choix de s’éloigner de std::ptrdiff_t
. Lorsque nous traitons avec du code vectorisé, il y a une nette différence, et plus petit, mieux c'est, mais c'est une autre histoire, et il n'y aurait aucune raison de rester avec int
. Dans ces cas, je vous recommanderais de passer aux types de taille fixe de C++.
Il y a eu une discussion sur les principes directeurs C++ Core:
https://github.com/isocpp/CppCoreGuidelines/pull/1115
Herb Sutter a écrit que gsl::index
sera ajouté (à l'avenir peut-être std::index
), qui sera défini comme ptrdiff_t
.
hsutter a commenté le 26 Dec 2017 •
(Merci à de nombreux experts du WG21 pour leurs commentaires et remarques dans cette note.)
Ajouter le typedef suivant à GSL
namespace gsl { using index = ptrdiff_t; }
et recommande
gsl::index
pour tous les index/indices/tailles de conteneurs.Justification
Les directives recommandent l’utilisation d’un type signé pour les indices/indices. Voir ES.100 à ES.107. C++ utilise déjà des entiers signés pour les indices de tableau.
Nous voulons pouvoir apprendre aux gens à écrire un "nouveau code propre et moderne" qui soit simple, naturel, sans avertissement à des niveaux d’alerte élevés, et ne nous oblige pas à écrire une note de bas de page "piège" sur le code simple.
Si nous n'avons pas un mot adoptable court comme
index
qui soit compétitif avecint
etauto
, les gens utiliseront toujoursint
etauto
et obtiendront leur bogues. Par exemple, ils écrirontfor(int i=0; i<v.size(); ++i)
oufor(auto i=0; i<v.size(); ++i)
qui ont des bugs de taille 32 bits sur les plates-formes largement utilisées, etfor(auto i=v.size()-1; i>=0; ++i)
qui ne fonctionne tout simplement pas. Je ne pense pas que nous puissions enseignerfor(ptrdiff_t i = ...
avec un visage impassible 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 présente presque tous les avantages d'un type arithmétique saturé non signé, à la différence près queptrdiff_t
permet toujours au style de boucle omniprésentefor(ptrdiff_t i=0; i<v.size(); ++i)
d'émettre des asymétries signée/non signée suri<v.size()
(et de même pouri!=v.size()
) pour les conteneurs STL actuels. (Si une future STL change son type de taille pour être signée, même ce dernier inconvénient disparaîtra.)Cependant, il serait sans espoir (et embarrassant) d’essayer d’apprendre aux gens à écrire systématiquement
for (ptrdiff_t i = ... ; ... ; ...)
. (Même les principes directeurs l'utilisent actuellement à un seul endroit et il s'agit d'un "mauvais" exemple sans rapport avec l'indexation ".)Par conséquent, nous devrions fournir
gsl::index
(qui peut ensuite être proposé pour examen en tant questd::index
) en tant que typedef pourptrdiff_t
, afin que nous puissions, espérons-le (et non embarrassant), enseigner aux gens à écrire régulièrement pour(index i = ... ; ... ; ...)
.Pourquoi ne pas simplement dire aux gens d'écrire
ptrdiff_t
? Parce que nous pensons qu'il serait embarrassant de dire aux gens que c'est ce que vous devez faire en C++, et même si nous le faisions, les gens ne le feraient pas. L'écriture deptrdiff_t
est trop laide et non adoptable par rapport àauto
etint
. L'ajout du nomindex
a pour objectif de rendre aussi simple et attrayant que possible l'utilisation d'un type signé de taille correcte.
Edit: Plus de justification de Herb Sutter
Est-ce que
ptrdiff_t
est assez grand? Oui. Les conteneurs standard doivent déjà ne pas contenir plus d'éléments que ce qui peut être représenté parptrdiff_t
, car la soustraction de deux itérateurs doit tenir dans un type de différence.Mais
ptrdiff_t
est-il vraiment assez grand, si j'ai un tableau intégré dechar
oubyte
qui est plus grand que la moitié de la taille de la mémoire adresse et a donc plus d’éléments que ce qui peut être représenté dans unptrdiff_t
? Oui. C++ utilise déjà des entiers signés pour les indices de tableau. Utilisez doncindex
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 semblable à un tableau, qui est supérieur à la moitié de l'espace d'adressage et dont les éléments sontsizeof(1)
, et que vous prenez soin d'éviter les problèmes de troncature, continuez, puis utilisez unsize_t
pour les index dans ce conteneur très spécial uniquement. Ces bêtes sont très rares dans la pratique, et quand elles surviennent, elles ne sont souvent pas indexées directement par le code utilisateur. Par exemple, elles surviennent généralement dans un gestionnaire de mémoire. qui prend en charge l'allocation système et divise les allocations individuelles plus petites utilisées par ses utilisateurs, ou dans un MPEG ou similaire qui fournit sa propre interface; dans les deux cas, lesize_t
ne devrait être requis qu'en interne dans le gestionnaire de mémoire ou la classe MPEG la mise en oeuvre.)
J'arrive à cela du point de vue d'un ancien minuteur (pré-C++) ... On avait compris à l'époque que int
était le mot natif de la plate-forme et était susceptible de donner les meilleures performances.
Si vous aviez besoin de quelque chose de plus gros, vous l'utiliseriez et en paieriez le prix en 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 se situait dans la plage où int sur une plate-forme cible pouvait la prendre en charge et int sur une autre plate-forme cible ne le pouvait pas… nous avions alors défini nos définitions de taille de temps de compilation (avant de les normaliser, nous avons créé le nôtre).
Mais maintenant, 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 quel sera l'impact de votre choix sur les performances d'une plate-forme ou d'un compilateur futur inconnu. Comment savons-nous vraiment que uint64_t, par exemple, fonctionnera mieux ou moins bien que uint32_t sur une cible future particulière? Sauf si vous êtes un gourou des processeurs/compilateurs, vous ne ...
Donc… peut-être que c'est démodé, mais à moins d'écrire du code pour un environnement contraint comme Arduino, etc. J'utilise toujours int
pour les valeurs d'usage général que je sais être dans int
taille sur tous objectifs raisonnables pour l’application que j’écris. Et le compilateur le prend à partir de là ... De nos jours, cela signifie généralement 32 bits signés. Même si l’on suppose que la taille d’entier minimale est 16 bits, elle couvre la plupart des cas d’utilisation.
La plupart des programmes ne vivent pas et ne meurent pas au bord de quelques cycles de processeur, et int
est très facile à écrire. Toutefois, si vous êtes sensible aux performances, je suggère d'utiliser les types entiers à largeur fixe définis dans <cstdint>
, tels que int32_t
ou uint64_t
. Celles-ci ont l'avantage d'être très claires quant au comportement souhaité en ce qui concerne la signature ou non signature, 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 la taille indiquée, mais pourraient l'être davantage si cela améliore les performances.
Aucune raison formelle d'utiliser int
. Cela ne correspond à rien de sain selon la norme. Pour les index, vous voulez presque toujours 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 vous a donné un coup de pied dans le dos. Les codeurs sont aussi des gens, n'apportez pas trop de laideur dans leur vie. Je préférerais utiliser certains types facilement typés comme long
ouindex
au lieu de std::ptrdiff_t
.
C'est un peu basé sur l'opinion, mais hélas, la question le demande un peu aussi.
Tout d’abord, vous parlez d’entiers et d’indices comme s’ils étaient identiques, ce qui n’est pas le cas. Pour toute chose telle que "nombre entier, ne sachant pas quelle taille" , utiliser simplement 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, ça va.
Pour les indices de tableau, c'est une autre histoire.
Il n’existe à ce jour qu’un seul élément formellement correct, à savoir _std::size_t
_. Dans l’avenir, il se peut qu’un _std::index_t
_ clarifie l’intention au niveau de la source, mais jusqu’à présent, ce n’est pas le cas.
_std::ptrdiff_t
_ en tant qu’indice "fonctionne" mais est tout aussi incorrect que int
car il permet des index négatifs.
Oui, cela se produit ce que M. Sutter juge correct, mais je me permets d’en différer. Oui, au niveau des instructions de la langue d'assemblage, cela est pris en charge très bien, mais je m'objecte toujours. La norme dit:
8.3.4/6: _
E1[E2]
_ est identique à*((E1)+(E2))
[...] En raison des règles de conversion applicables à _+
_, si _E1
_ est un tableau et _E2
_ un entier, puis _E1[E2]
_ fait référence au _E2
_- ème membre de _E1
_.
5.7/5: [...] Si l'opérande de pointeur et le résultat pointent tous deux sur des éléments du même objet tableau, ou si l'un d'eux est passé après le dernier élément de l'objet tableau [...], - le comportement n'est pas défini.
Un abonnement à un tableau fait référence au E2
_- ÈME MEMBRE DE _E1
. Il n’existe pas d’élément négatif d’un tableau. Mais plus important encore, l'arithmétique du pointeur avec une expression additive négative invoque un comportement indéfini .
En d'autres termes: les index signés de toute taille sont un mauvais choix . Les indices ne sont pas signés. Oui, les index signés fonctionnent , mais ils ont toujours tort.
Maintenant, bien que _size_t
_ soit par définition le bon choix (un type entier non signé suffisamment grand pour contenir la taille d’un objet), il peut être discutable de savoir si est vraiment bon choix pour le cas moyen, ou par défaut.
Soyez honnête, quand était la dernière fois que vous avez créé un tableau avec 1019 éléments?
J'utilise personnellement _unsigned int
_ par défaut, car les 4 milliards d'éléments que cela permet sont suffisants pour (presque) toutes les applications et poussent déjà l'ordinateur de l'utilisateur moyen assez proche de sa limite (si vous vous abonnez simplement à un tableau d’entiers, qui suppose 16 Go de mémoire contiguë allouée). Personnellement, j’estime ridicule que le défaut d’index 64 bits soit un défaut.
Si vous programmez une base de données relationnelle ou un système de fichiers, alors oui, vous aurez besoin des index 64 bits. Mais pour le programme "normal" moyen, les index 32 bits sont tout à fait suffisants et ne consomment que la moitié moins de mémoire.
En conservant beaucoup plus qu'une poignée d'indices, et si je peux me permettre (parce que les tableaux ne sont pas plus grands que 64k éléments), je descends même à _uint16_t
_. Non, je ne plaisante pas là-bas.
Le stockage est-il vraiment un problème? Il est ridicule de faire avare deux ou quatre octets sauvés, n'est-ce pas! Et bien non...
La taille peut être un problème pour les pointeurs, alors elle peut également l'être pour les index. L'ABI x32 n'existe pas sans raison. Vous ne remarquerez pas les frais généraux d'index inutilement volumineux si vous n'en avez que quelques-uns au total (tout comme les pointeurs, ils seront dans les registres de toute façon, personne ne remarquera s'ils ont une taille de 4 ou 8 octets).
Mais imaginons par exemple une carte de créneaux dans laquelle vous stockez un index pour chaque élément (en fonction de la mise en œuvre, deux index par élément). Oh zut, ça fait vraiment une grosse différence que vous frappiez L2 à chaque fois, ou que vous ayez un cache manqué à chaque accès! Le plus gros n'est pas toujours le meilleur.
À la fin de la journée, vous devez vous demander ce que vous payez et ce que vous recevez en retour. Dans cet esprit, ma recommandation de style serait:
Si cela ne vous coûte rien, car vous n’avez que par exemple un pointeur et quelques index à conserver, puis utilisez simplement ce qui est formellement correct (ce serait _size_t
_). Formellement correct est bon, correct fonctionne toujours, il est lisible et intuitif, et correct est ... jamais faux .
Si, toutefois, il vous coûte (vous avez peut-être plusieurs centaines, mille ou dix mille indices), et ce que vous récupérez ne vaut rien (car par exemple, vous ne pouvez même pas stocker 220 éléments, donc si vous pouvez vous abonner 232 ou 264 ne fait aucune différence), vous devriez réfléchir à deux fois avant de gaspiller trop.
Sur la plupart des architectures 64 bits modernes, int
correspond à 4 octets et ptrdiff_t
à 8 octets. Si votre programme utilise beaucoup d'entiers, utiliser ptrdiff_t
au lieu de int
pourrait doubler la mémoire requise par votre programme.
Considérez également que les performances des mémoires mettent souvent en défaut les processeurs modernes. L'utilisation d'entiers de 8 octets signifie également que le cache de votre CPU contient désormais deux fois moins d'éléments qu'auparavant. Il doit donc attendre plus souvent que la mémoire principale soit lente (ce qui peut facilement prendre plusieurs centaines de cycles).
Dans de nombreux cas, le coût d'exécution des opérations de "conversion de 32 à 64 bits" est complètement réduit à néant par les performances de la mémoire.
C’est donc une raison pratique pour que int
soit toujours populaire sur les machines 64 bits.
Je vous conseille de ne pas trop regarder la sortie du langage d'assemblage, de ne pas trop vous soucier de la taille exacte de chaque variable et de ne pas dire des choses comme "le compilateur se sent à l'aise avec". (Je ne sais vraiment pas ce que vous entendez par ce dernier.)
Pour les entiers de la variété garden, ceux dont la plupart des programmes sont remplis, plain int
est supposé être un bon type à utiliser. C'est censé être la taille de mot naturelle de la machine. Il est supposé être efficace à utiliser, ne gaspillant pas de mémoire inutile et n'induisant pas de nombreuses conversions supplémentaires lors du déplacement entre des registres de mémoire et de calcul.
Il est vrai qu’il existe de nombreuses utilisations plus spécialisées pour lesquelles plain int
ne convient plus. En particulier, la taille des objets, le nombre d'éléments et les index dans les tableaux sont presque toujours size_t
. Mais cela ne signifie pas que tous les entiers doivent être size_t
!
Il est également vrai que les mélanges de types signés et non signés, ainsi que les mélanges de types de tailles différentes, peuvent poser problème. Mais la plupart d'entre eux sont bien pris en charge par les compilateurs modernes et par les avertissements qu'ils émettent pour les combinaisons non sécurisées. Donc, tant que vous utilisez un compilateur moderne et que vous prêtez une attention particulière à ses avertissements, vous n'avez pas besoin de choisir un type non naturel simplement pour essayer d'éviter les problèmes d'inadéquation des types.
Je ne pense pas qu'il y ait réelle raison d'utiliser int
.
Comment choisir le type entier?
std::ptrdiff_t
(le seul problème est lorsque la taille est supérieure à PTRDIFF_MAX
, ce qui est rare en pratique)intXX_t
ou int(_least)/(_fast)XX_t
.Ces règles couvrent tous les usages possibles de int
et donnent une meilleure solution:
int
n'est pas bon pour stocker des éléments liés à la mémoire, car sa plage peut être inférieure à celle d'un index (ceci n'est pas théorique: pour les machines 64 bits, int
est généralement 32 bits, donc avec int
, vous ne pouvez gérer que 2 milliards d'éléments)int
ne convient pas pour stocker des entiers "généraux", car son étendue peut être plus petite que nécessaire (un comportement indéfini se produit si l'intervalle n'est pas suffisant), ou au contraire, son étendue peut être beaucoup plus grande que nécessaire (mémoire). est gâché)La seule raison pour laquelle on pourrait utiliser un int
, si on fait un calcul, et sait que la plage est comprise dans [-32767; 32767] (la norme ne garantit que cette plage. Notez cependant que les implémentations sont libres de int
s, et ils le font généralement. Actuellement, int
est en 32 bits sur de nombreuses plates-formes).
Comme les types std
mentionnés sont un peu fastidieux à écrire, on pourrait typedef
les raccourcir (j'utilise s8
/u8
/.../s64
/u64
, et spt
/upt
("type de pointeur signé (non)" ") pour ptrdiff_t
/size_t
. Je l'utilise ces typedefs depuis 15 ans, et je n'ai jamais écrit un seul int
depuis ...).
Plus facile à taper, je suppose? Mais vous pouvez toujours typedef
.
De nombreuses API utilisent int, y compris des parties de la bibliothèque standard. Cela a toujours causé des problèmes, par exemple lors de la transition vers des tailles de fichier 64 bits.
En raison des règles de promotion de type par défaut, les types plus étroits qu'int peuvent être étendus à int ou unsigned int, sauf si vous ajoutez des conversions explicites dans un grand nombre d'emplacements. De nombreux types différents peuvent être plus étroits que int dans certaines implémentations. Donc, si vous vous souciez de la portabilité, c’est un mal de tête mineur.
J'utilise aussi ptrdiff_t
pour les index, la plupart du temps. (Je conviens avec Google que les indices non signés sont un attracteur de bogues.) Pour d’autres types de mathématiques, il ya int_fast64_t
. int_fast32_t
, etc., qui sera aussi bon ou meilleur que int
. Presque aucun système réel, à l'exception de quelques Unices disparus du siècle dernier, utilise ILP64, mais il existe de nombreux processeurs pour lesquels vous souhaiteriez des calculs en 64 bits. Et un compilateur est techniquement autorisé, en standard, à interrompre votre programme si votre int
est supérieur à 32 767.
Cela dit, tout compilateur C digne de ce nom sera testé sur beaucoup de code qui ajoute un int
à un pointeur dans une boucle interne. Donc, il ne peut rien faire de trop bête. Le pire scénario sur le matériel actuel est qu'il nécessite une instruction supplémentaire pour signer-étendre une valeur signée de 32 bits à 64 bits. Mais, si ce que vous voulez vraiment, c'est le calcul mathématique le plus rapide, les calculs les plus rapides pour les valeurs d'une magnitude comprise entre 32 kibi et 2 gibi, ou le moins de mémoey perdu, vous devez dire ce que vous voulez dire, sans laisser deviner le compilateur.
Je suppose que dans 99% des cas, il n'y a aucune raison d'utiliser int
(ou un entier signé d'autres tailles). Cependant, il existe encore des situations dans lesquelles utiliser int
est une bonne option.
Une performance:
Une différence entre int
et _size_t
_ est que _i++
_ peut être un comportement non défini pour int
- si i
est _MAX_INT
_. Cela pourrait en fait être une bonne chose car le compilateur pourrait utiliser ce comportement indéfini pour accélérer les choses.
Par exemple, dans cette question , la différence était d'environ le facteur 2 entre l'exploitation du comportement indéfini et l'utilisation de l'indicateur de compilation -fwrapv
qui interdit cet exploit.
Si mon cheval de travail devient deux fois plus rapide en utilisant int
s - bien sûr, je l'emploierai
B) Code moins sujet aux erreurs
Les boucles inversées avec _size_t
_ semblent étranges et constituent une source d’erreurs (j’espère avoir bien compris):
_for(size_t i = N-1; i < N; i--){...}
_
En utilisant
_for(int i = N-1; i >= 0; i--){...}
_
vous mériterez la gratitude des programmeurs C++ moins expérimentés, qui devront gérer votre code un jour.
C) Conception utilisant des index signés
En utilisant int
comme indices, vous pouvez signaler des valeurs incorrectes/hors limites avec des valeurs négatives, ce qui est pratique et peut permettre d'obtenir un code plus clair.
"find index d'un élément dans un tableau" peut renvoyer _-1
_ si l'élément n'est pas présent. Pour détecter cette "erreur", vous n'avez pas besoin de connaître la taille du tableau.
la recherche binaire peut renvoyer un index positif si l'élément est dans le tableau et _-index
_ pour la position où l'élément serait inséré dans le tableau (et ne fait pas partie du tableau).
Clairement, la même information pourrait être codée avec des valeurs d’index positives, mais le code devient un peu moins intuitif.
Clairement, il y a aussi des raisons de choisir int
sur _std::ptrdiff_t
_ - l'un d'eux est la bande passante mémoire. Il existe de nombreux algorithmes liés à la mémoire. Pour eux, il est important de réduire la quantité de mémoire transférée de RAM à la mémoire cache.
Si vous savez que tous les nombres sont inférieurs à _2^31
_, il serait avantageux d’utiliser int
car sinon, la moitié du transfert de mémoire ne consisterait qu’en écriture _0
_ que vous connaissez déjà, Ils sont là.
Un exemple est constitué par les matrices de lignes fragmentées compressées (crs). Leurs index sont stockés sous la forme ints
et non pas _long long
_. Etant donné que de nombreuses opérations avec des matrices creuses sont liées à la mémoire, l’utilisation de 32 ou 64 bits est vraiment différente.