Les types à largeur variable ont-ils été remplacés par des types fixes dans le C moderne?

21

Je suis tombé sur un point intéressant aujourd'hui dans une revue de Code Review . @Veedrac a recommandé dans cette réponse que les types de taille variable (par exemple intet long) soient remplacés par des types de taille fixe comme uint64_tet uint32_t. Citation des commentaires de cette réponse:

Les tailles de int et long (et donc les valeurs qu'elles peuvent contenir) dépendent de la plate-forme. D'un autre côté, int32_t a toujours une longueur de 32 bits. Utiliser int signifie simplement que votre code fonctionne différemment sur différentes plates-formes, ce qui n'est généralement pas ce que vous voulez.

Le raisonnement derrière la norme ne fixant pas les types courants est partiellement expliqué ici par @supercat. C a été écrit pour être portable à travers les architectures, contrairement à l'assemblage qui était habituellement utilisé pour la programmation des systèmes à l'époque.

Je pense que l'intention de conception était à l'origine que chaque type autre que int soit la plus petite chose qui puisse gérer des nombres de différentes tailles, et que ce soit la taille "polyvalente" la plus pratique qui puisse gérer +/- 32767.

Quant à moi, j'ai toujours utilisé intet pas vraiment inquiété les alternatives. J'ai toujours pensé que c'est le type le plus performant, la fin de l'histoire. Le seul endroit où je pensais qu'une largeur fixe serait utile est lors du codage des données pour le stockage ou pour le transfert sur un réseau. J'ai rarement vu des types à largeur fixe dans le code écrit par d'autres non plus.

Suis-je coincé dans les années 70 ou y a-t-il vraiment une justification pour l'utiliser intà l'ère de C99 et au-delà?

jacwah
la source
1
Une partie des gens imite simplement les autres. Je crois que la majorité du code à bits fixes a été rendue sans conscience. Aucune raison de définir la taille ni de ne pas. J'ai du code créé principalement sur des plates-formes 16 bits (MS-DOS et Xenix des années 80), qui se compilent et s'exécutent aujourd'hui sur n'importe quel 64 et bénéficient de la nouvelle taille de mot et de l'adressage, il suffit de le compiler. C'est-à-dire que la sérialisation pour exporter / importer des données est une conception d'architecture très importante pour la garder portable.
Luciano

Réponses:

7

Il existe un mythe commun et dangereux selon lequel des types comme les uint32_tprogrammeurs n'ont pas à se soucier de la taille de int. Bien qu'il soit utile que le Comité des normes définisse un moyen de déclarer des entiers avec une sémantique indépendante de la machine, les types non signés comme la uint32_tsémantique sont trop lâches pour permettre l'écriture du code d'une manière à la fois propre et portable; en outre, les types signés comme int32ont une sémantique qui, pour de nombreuses applications, sont définis de manière inutile et précise et empêchent donc ce qui serait autrement des optimisations utiles.

Considérez, par exemple:

uint32_t upow(uint32_t n, uint32_t exponent)
{
  while(exponent--)
    n*=n;
  return n;
}

int32_t spow(int32_t n, uint32_t exponent)
{
  while(exponent--)
    n*=n;
  return n;
}

Sur les machines où intsoit ne peut pas contenir 4294967295, soit 18446744065119617025, la première fonction sera définie pour toutes les valeurs de net exponent, et son comportement ne sera pas affecté par la taille de int; en outre, la norme n'exigera pas qu'elle produise un comportement différent sur les machines de n'importe quelle taille de int Certaines valeurs de net exponent, cependant, l'amènera à invoquer un comportement intindéfini sur les machines où 4294967295 est représentable en tant que mais pas 18446744065119617025.

La deuxième fonction produira un comportement indéfini pour certaines valeurs de net exponentsur les machines qui intne peuvent pas contenir 4611686014132420609, mais produira un comportement défini pour toutes les valeurs de net exponentsur toutes les machines où elle le peut (les spécifications pour int32_timpliquent que le comportement d'habillage en complément à deux sur les machines où il est plus petit que int).

Historiquement, même si la norme ne disait rien sur ce que les compilateurs devraient faire avec le intdébordement upow, les compilateurs auraient toujours produit le même comportement que s'ils intavaient été suffisamment grands pour ne pas déborder. Malheureusement, certains compilateurs plus récents peuvent chercher à «optimiser» les programmes en éliminant les comportements non requis par la norme.

supercat
la source
3
Toute personne souhaitant implémenter manuellement pow, rappelez-vous que ce code n'est qu'un exemple et ne répond pas exponent=0!
Mark Hurd
1
Je pense que vous devriez utiliser l'opérateur de décrémentation du préfixe et non le suffixe, actuellement il fait 1 multiplication supplémentaire, par exemple, exponent=1cela entraînera la multiplication de n par lui-même une fois, car la décrémentation est effectuée après la vérification, si l'incrémentation est effectuée avant la vérification ( ie --exponent), aucune multiplication ne sera effectuée et n lui-même sera retourné.
ALXGTV
2
@MarkHurd: La fonction est mal nommée, car ce qu'elle calcule réellement N^(2^exponent), mais les calculs de la forme N^(2^exponent)sont souvent utilisés dans le calcul des fonctions d'exponentiation, et l'exponentiation mod-4294967296 est utile pour des choses comme le calcul du hachage de la concaténation de deux chaînes dont les hachages sont connus.
supercat
1
@ALXGTV: La fonction était censée illustrer quelque chose qui calculait quelque chose lié à la puissance. Ce qu'il calcule réellement est N ^ (exposant 2 ^), qui fait partie du calcul efficace de l'exposant N ^, et peut bien échouer même si N est petit (la multiplication répétée de a uint32_tpar 31 ne donnera jamais UB, mais l'efficacité façon de calculer 31 ^ N implique des calculs de 31 ^ (2 ^ N), ce qui sera
supercat
Je ne pense pas que ce soit un bon argument. Le but n'est pas de rendre les fonctions définies pour toutes les entrées, sensibles ou non; c'est de pouvoir raisonner sur les tailles et les débordements. int32_tParfois, avoir défini un débordement et parfois non, ce que vous semblez mentionner, semble d'une importance minime par rapport au fait qu'il me permet de raisonner pour empêcher le débordement en premier lieu. Et si vous voulez un débordement défini, il y a de fortes chances que vous souhaitiez que le résultat modulo ait une valeur fixe - vous utilisez donc des types à largeur fixe de toute façon.
Veedrac
4

Pour les valeurs étroitement liées aux pointeurs (et donc à la quantité de mémoire adressable) telles que les tailles de mémoire tampon, les index de tableau et Windows ' lParam, il est logique d'avoir un type entier avec une taille dépendante de l'architecture. Ainsi, les types de taille variable sont toujours utiles. Voilà pourquoi nous avons les typedefs size_t, ptrdiff_t, intptr_t, etc. Ils ont à être typedefs car aucun des types entier intégré C besoin d' être de taille pointeur.

Donc , la question est vraiment de savoir si char, short, int, longet long longsont encore utiles.

IME, il est encore courant que les programmes C et C ++ soient utilisés intpour la plupart des choses. Et la plupart du temps (c'est-à-dire lorsque vos chiffres sont dans la plage ± 32 767 et que vous n'avez pas d'exigences de performances rigoureuses), cela fonctionne très bien.

Mais que se passe-t-il si vous devez travailler avec des nombres compris entre 17 et 32 ​​bits (comme les populations des grandes villes)? Vous pourriez utiliser int, mais ce serait coder en dur une dépendance de plate-forme. Si vous souhaitez respecter strictement la norme, vous pouvez utiliser longce qui est garanti être d'au moins 32 bits.

Le problème est que la norme C ne spécifie aucune taille maximale pour un type entier. Il existe des implémentations sur lesquelles longest 64 bits, ce qui double votre utilisation de la mémoire. Et si ces longéléments se trouvent être des éléments d'un tableau contenant des millions d'éléments, vous allez battre la mémoire comme un fou.

Donc, ni n'est un type approprié à utiliser ici si vous voulez que votre programme soit à la fois multi-plateforme et économe en mémoire. Entrez .intlongint_least32_t

  • Votre compilateur I16L32 vous donne un 32 bits long, évitant les problèmes de troncature deint
  • Votre compilateur I32L64 vous donne un 32 bits int, évitant le gaspillage de mémoire du 64 bits long.
  • Votre compilateur I36L72 vous donne un 36 bits int

OTOH, supposons que vous n'ayez pas besoin de nombres ou de tableaux énormes mais que vous avez besoin de vitesse. Et intpeut être assez grand sur toutes les plates-formes, mais ce n'est pas nécessairement le type le plus rapide: les systèmes 64 bits ont généralement encore 32 bits int. Mais vous pouvez utiliser int_fast16_tet obtenir le type « plus rapide », que ce soit int, longou long long.

Il existe donc des cas d'utilisation pratiques pour les types de <stdint.h>. Les types entiers standard ne signifient rien. Surtout long, qui peut être de 32 ou 64 bits, et peut être ou ne pas être suffisamment grand pour contenir un pointeur, selon le caprice des rédacteurs du compilateur.

dan04
la source
Un problème avec des types comme uint_least32_test que leurs interactions avec d'autres types sont encore plus faiblement spécifiées que celles de uint32_t. À mon humble avis, la norme devrait définir des types tels que uwrap32_tet unum32_t, avec la sémantique que tout compilateur qui définit le type uwrap32_t, doit promouvoir en tant que type non signé dans essentiellement les mêmes cas qu'il serait promu s'il intétait de 32 bits, et tout compilateur qui définit le type unum32_tdoit s'assurer que les promotions arithmétiques de base le convertissent toujours en un type signé capable de conserver sa valeur.
supercat
De plus, la norme pourrait également définir des types dont le stockage et l'aliasing étaient compatibles avec intN_tet uintN_t, et dont les comportements définis seraient cohérents avec intN_tet uintN_t, mais qui donneraient aux compilateurs une certaine liberté dans les valeurs de code de cas attribuées en dehors de leur plage [permettant une sémantique similaire à celles qui étaient peut-être destiné à uint_least32_t, mais sans incertitudes comme si l'ajout d'un uint_least16_tet d'un int32_tdonnerait un résultat signé ou usigné.
supercat