web-dev-qa-db-fra.com

Y a-t-il une raison de ne pas utiliser de types entiers à largeur fixe (par exemple uint8_t)?

En supposant que vous utilisez un compilateur qui prend en charge C99 (ou même simplement stdint.h), y a-t-il une raison de ne pas utiliser des types entiers à largeur fixe tels que uint8_t?

L'une des raisons que je connais est qu'il est beaucoup plus logique d'utiliser chars lorsque vous traitez des caractères au lieu d'utiliser (u)int8_ts, comme mentionné dans cette question .

Mais si vous prévoyez de stocker un numéro, quand voudriez-vous utiliser un type dont vous ne savez pas la taille? C'est à dire. Dans quelle situation souhaitez-vous enregistrer un numéro dans un unsigned short sans savoir s'il s'agit de 8, 16, voire 32 bits, au lieu d'utiliser un uint16t?

Suite à cela, est-il considéré comme une meilleure pratique d'utiliser des entiers à largeur fixe, ou d'utiliser les types d'entiers normaux et de ne jamais rien supposer et d'utiliser sizeof partout où vous avez besoin de savoir combien d'octets ils utilisent?

52
DanielGibbs

Il est en fait assez courant de stocker un nombre sans avoir besoin de connaître la taille exacte du type. Il y a beaucoup de quantités dans mes programmes que je peux raisonnablement supposer ne dépassera pas 2 milliards de dollars, ou imposez-les. Mais cela ne signifie pas que j'ai besoin d'un type 32 bits exact pour les stocker, tout type pouvant compter au moins 2 milliards me convient.

Si vous essayez d'écrire du code très portable, vous devez garder à l'esprit que les types à largeur fixe sont tous facultatif.

Sur une implémentation C99 où CHAR_BIT Est supérieur à 8, Il n'y a pas de int8_t. La norme interdit son existence car il devrait avoir des bits de remplissage et les types intN_t Sont définis pour ne pas avoir de bits de remplissage (7.18.1.1/1). uint8_t Est donc également interdit car (merci, ouah) une implémentation n'est pas autorisée à définir uint8_t Sans int8_t.

Donc, dans un code très portable, si vous avez besoin d'un type signé capable de contenir des valeurs jusqu'à 127, vous devez utiliser l'un des signed char, int, int_least8_t Ou int_fast8_t Selon que vous voulez demander au compilateur de le faire:

  • travailler en C89 (signed char ou int)
  • éviter les promotions entières surprenantes dans les expressions arithmétiques (int)
  • petit (int_least8_t ou signed char)
  • rapide (int_fast8_t ou int)

Il en va de même pour un type non signé jusqu'à 255, avec unsigned char, unsigned int, uint_least8_t Et uint_fast8_t.

Si vous avez besoin de l'arithmétique modulo-256 dans un code très portable, vous pouvez soit prendre le module vous-même, masquer des bits ou jouer à des jeux avec des champs de bits.

En pratique, la plupart des gens n'ont jamais besoin d'écrire du code aussi portable. Pour le moment, CHAR_BIT > 8 N'apparaît que sur du matériel à usage spécial, et votre code à usage général ne sera pas utilisé dessus. Bien sûr, cela pourrait changer à l'avenir, mais si c'est le cas, je soupçonne qu'il y a tellement de code qui fait des hypothèses sur Posix et/ou Windows (qui garantissent tous les deux CHAR_BIT == 8), Celui traitant de la non-portabilité de votre code sera une petite partie d'un gros effort pour porter le code sur cette nouvelle plate-forme. Une telle implémentation devra probablement se soucier de la façon de se connecter à Internet (qui traite en octets), bien avant de se demander comment obtenir votre code et le faire fonctionner :-)

Si vous supposez que CHAR_BIT == 8 De toute façon, je ne pense pas qu'il y ait une raison particulière d'éviter (u)int8_t Autre que si vous voulez que le code fonctionne en C89. Même en C89, il n'est pas si difficile de trouver ou d'écrire une version de stdint.h Pour une implémentation particulière. Mais si vous pouvez facilement écrire votre code pour exiger uniquement que le type puisse contenir 255, Plutôt que de l'exiger ne peut pas tenir 256, alors vous pourriez aussi bien éviter la dépendance à CHAR_BIT == 8.

34
Steve Jessop

Un problème qui n'a pas encore été mentionné est que, bien que l'utilisation de types entiers de taille fixe signifie que la taille de ses variables ne changera pas si les compilateurs utilisent des tailles différentes pour int, long, et ainsi de suite, cela ne garantit pas nécessairement que le code se comportera de manière identique sur les machines avec différentes tailles entières, même lorsque les tailles sont définies.

Par exemple, étant donné la déclaration uint32_t i;, Le comportement de l'expression (i-1) > 5 Lorsque i est nul variera selon que uint32_t Est plus petit que int. Sur les systèmes où par ex. int est de 64 bits (et uint32_t est quelque chose comme long short), la variable i serait promue en int; la soustraction et la comparaison seraient effectuées telles que signées (-1 est inférieur à 5). Sur les systèmes où int est de 32 bits, la soustraction et la comparaison seraient effectuées comme unsigned int (La soustraction donnerait un très grand nombre, supérieur à cinq).

Je ne sais pas dans quelle mesure le code repose sur le fait que les résultats intermédiaires d'expressions impliquant des types non signés doivent être encapsulés même en l'absence de transtypages (à mon humble avis, si un comportement d'encapsulation était souhaité, le programmeur aurait dû inclure un transtypage) (uint32_t)(i-1) > 5) Mais la norme ne permet actuellement aucune latitude. Je me demande quels problèmes seraient posés si une règle selon laquelle au moins autorisé un compilateur pour promouvoir les opérandes en un type entier plus long en l'absence de transtypages ou de coercitions de type [par exemple étant donné uint32_t i,j, une affectation comme j = (i+=1) >> 1; serait nécessaire pour couper le débordement, tout comme j = (uint32_t)(i+1) >> 1;, mais j = (i+1)>>1 ne le ferait pas]? Ou, d'ailleurs, combien il serait difficile pour les fabricants de compilateurs de garantir que toute expression de type intégral dont les résultats intermédiaires pourraient tous s'inscrire dans le plus grand type signé et n'impliquerait pas de décalages à droite par des quantités non constantes, produirait le même comme si tous les calculs avaient été effectués sur ce type? Il me semble plutôt capricieux que sur une machine où int est 32 bits:

 uint64_t a, b, c; 
 ... 
 a & = ~ 0x40000000; 
 b & = ~ 0x80000000; 
 c & = ~ 0x100000000; 

efface un bit chacun de a et c, mais efface les 33 premiers bits de b; la plupart des compilateurs ne donnent aucune indication que quelque chose est "différent" de la deuxième expression.

12
supercat

Il est vrai que la largeur d'un type d'entier standard peut changer d'une plateforme à l'autre mais pas son minimum width.

Par exemple, la norme C spécifie qu'un int est au moins 16-bit et un long est au moins 32-bit large.

Si vous n'avez pas de contrainte de taille lors du stockage de vos objets, vous pouvez laisser cela à l'implémentation. Par exemple, si votre valeur maximale signée tient dans un 16-bit vous pouvez simplement utiliser un int. Vous laissez ensuite l'implémentation avoir le dernier mot de ce que c'est la largeur naturelle int pour l'architecture que l'implémentation cible.

7
ouah

Vous ne devez utiliser les types de largeur fixes que lorsque vous faites une hypothèse sur la largeur.

uint8_t et unsigned char sont les mêmes sur la plupart des plates-formes, mais pas sur toutes. En utilisant uint8_t met l'accent sur le fait que vous supposez une architecture avec 8 bits char et que vous ne compilez pas sur d'autres, c'est donc une fonctionnalité.

Sinon, j'utiliserais la "sémantique" typedef telle que size_t, uintptr_t, ptrdiff_t car ils reflètent bien mieux ce que vous avez en tête avec les données. Je n'utilise presque jamais directement les types de base, int uniquement pour les retours d'erreur, et je ne me souviens pas avoir déjà utilisé short.

Edit: Après une lecture attentive de C11, je conclus que uint8_t, s'il existe, doit être unsigned char et ne peut pas être simplement char même si ce type n'est pas signé. Cela vient de l'exigence de 7.20.1 p1 que tous les intN_t et uintN_t doit être de type correspondant signé et non signé. La seule paire de ce type pour les types de caractères est signed char et unsigned char.

4
Jens Gustedt

Le code doit révéler au lecteur occasionnel (et au programmeur lui-même) ce qui est important. Est-ce juste un entier ou entier non signé ou même entier signé. Il en va de même pour la taille. Est-il vraiment important pour l'algorithme qu'une variable soit par défaut de 16 bits? Ou s'agit-il simplement d'une microgestion inutile et d'une tentative infructueuse d'optimisation?

C'est ce qui fait de la programmation un art - pour montrer ce qui est important.

3
Aki Suihkonen

Il y a plusieurs raisons pour lesquelles on voudrait utiliser le, appelons-les types sémantiques comme int ou char sur les types à largeur fixe comme uint8_t:

Correspondant à l'API existante

La bibliothèque C standard utilise char* Partout. Pourquoi confondre les utilisateurs (et introduire d'éventuels bogues?) En utilisant un type différent lorsque vous parlez à cette API?

De même, les chaînes de format printf() sont définies en fonction de ces types sémantiques. Si vous souhaitez imprimer un type de taille fixe, vous avez besoin de macros comme PRIu64 Etc. dans stdint.h Pour vous permettre d'obtenir la bonne chaîne de format pour imprimer un type de taille fixe avec l'ancien printf format des chaînes.

La vitesse

Les types sémantiques sont généralement choisis de sorte qu'ils fonctionnent le mieux pour les caractéristiques de performance du processeur actuel. Ils peuvent être remplacés par des tailles légèrement plus grandes que celles que vous auriez choisies car c'est la taille du registre sur votre CPU et vous économiserez des conversions inutiles, etc.

Ces jours-ci, c'est une réponse contestée ... c'était l'intention initiale, mais en raison du fait que stdint n'était pas disponible au début de C/C++, de nombreuses plates-formes (comme Windows 32 bits ou macOS X) vient de garantir la taille de int et long. Ainsi, lors du déplacement 64 bits, certaines de ces tailles sont restées les mêmes (ce qui a conduit à de nouveaux types amusants comme long long, Entre autres). C'est pourquoi nous avons obtenu les types least et fast.

Portabilité du code

Les types sémantiques peuvent être plus grands sur une plate-forme 64 bits que sur une plate-forme 32 bits (par exemple pour permettre aux index de tableau de remplir toute la mémoire). Donc, si vous exécutez sur différentes plates-formes, l'utilisation d'un type sémantique (qui, selon ma définition, inclurait size_t Le cas échéant) au lieu d'une version fixe signifie que vous profitez d'un meilleur matériel et que vous n'ajoutez pas de limites arbitraires .

Bien sûr, cela ne rend que votre algorithme portable. Si vous devez sérialiser des données en octets et les échanger entre différentes plates-formes, cela peut rendre votre code portable, mais pas vos paquets réseau ou vos fichiers de sortie. Donc, dans ce cas, vous voudriez en fait vous en tenir aux types fixes pour que les données restent portables, au prix de votre code fonctionnant de manière insupportablement lente ou ne se compilant pas sur certaines plates-formes.

Commentaire: Ne me demandez pas pourquoi ils n'ont pas introduit de chaînes de format pour int64_t, int32_t Etc. Peut-être manquaient-ils de lettres? Peut-être que trop de bases de code ont défini leurs propres chaînes de format et se seraient cassées?

1
uliwitness