TL; DR: Pourquoi tout le monde ne crie-t-il pas: "N'utilisez pas short
, int
et long
sauf si vous en avez vraiment besoin, et vous ne le ferez probablement pas besoin de!"
Je comprends qu'en théorie, en utilisant les types short
, int
et long
, vous laissez le compilateur choisir la longueur la plus efficace pour le processeur donné.
Mais s'agit-il d'un cas d'optimisation prématurée à l'origine de tout mal?
Supposons que j'ai une variable entière dont je sais qu'elle contiendra toujours des nombres de 1 à 1000. Je crois comprendre que, en supposant que je ne m'inquiète pas de la différence de mémoire entre deux et quatre octets, les partisans de short
/int
/long
voudrait que je fasse de cette variable un int
parce que de cette façon le compilateur peut choisir 16 bits ou 32 bits selon ce qui est plus efficace pour le processeur. Si je l'avais fait un uint16_t
, le compilateur peut ne pas être en mesure de rendre un code aussi rapide.
Mais sur le matériel moderne, est-ce encore vrai? Ou plutôt, la vitesse qui va me gagner (le cas échéant), vaut-elle vraiment la possibilité beaucoup plus probable que l'utilisation d'un type imprécis conduise à un bug majeur dans mon programme? Par exemple, je peux utiliser int
tout au long de mon programme et la considérer comme représentant une valeur 32 bits car c'est ce qu'elle représente sur toutes les plateformes que j'ai utilisées au cours des 20 dernières années, mais mon code est ensuite compilé sur une plate-forme inhabituelle où int
fait deux octets et toutes sortes de bugs se produisent.
Et à part les bugs, cela semble juste être une manière énormément imprécise pour les programmeurs de parler des données. À titre d'exemple, voici la définition qui Microsoft donne en 2019 pour une structure GUID:
typedef struct _GUID {
unsigned long Data1;
unsigned short Data2;
unsigned short Data3;
unsigned char Data4[8];
} GUID;
En raison de ce qu'est un Uuid, ce long a pour signifier 32 bits, ces shorts ont pour signifier 16 bits, et que char a pour signifier 8 bits. Alors pourquoi continuer à parler dans ce langage imprécis de "court", "long" et (le ciel nous aide) "long long"?
Je comprends qu'en théorie, en utilisant les types short, int et long, vous laissez le compilateur choisir la longueur la plus efficace pour le processeur donné.
Ce n'est que partiellement vrai. Tous ces types ont une taille minimale garantie en ANSI C (AFAIK même en ANSI C89). Le code reposant uniquement sur ces tailles minimales est toujours portable. Les cas où la taille maximale d'un type est importante pour la portabilité sont beaucoup moins fréquents. Cela dit, j'ai vu (et écrit) beaucoup de code au cours des années où int
était supposé être au moins 32 bits, écrit clairement pour les environnements avec> = 32 bits au minimum.
Mais s'agit-il d'un cas d'optimisation prématurée [...]?
L'optimisation prématurée ne consiste pas seulement à optimiser la vitesse. Il s'agit d'investir un effort supplémentaire dans le code, et de rendre le code plus compliqué , pour une raison (souvent pathologique) "au cas où". "Juste au cas où cela pourrait être lent" n'est qu'une de ces raisons potentielles. Ainsi, éviter l'utilisation de int
"au cas où" il pourrait être porté sur une plate-forme 16 bits à l'avenir pourrait également être considéré comme une forme d'optimisation prématurée, lorsque ce type de portage ne se produira probablement jamais.
Cela dit, je pense que la partie que vous avez écrite sur int
est dans une certaine mesure correcte: au cas où il y aurait des preuves qu'un programme pourrait être porté d'une plate-forme 32 vers une plate-forme 16 bits, il serait préférable de ne pas compter sur int
ayant 32 bits, et pour utiliser soit long
, un type de données C99 spécifique comme int32_t
ou int_least32_t
partout où l'on ne sait pas si 16 bits suffisent ou non. On pourrait également utiliser un typedef
global pour définir int32_t
sur des plates-formes non conformes C99. Tout cela représente un petit effort supplémentaire (au moins pour enseigner à l'équipe quels types de données spéciaux ont été utilisés dans le projet et pourquoi).
Voir aussi cet ancien SO , pour lequel la réponse la plus élevée dit, la plupart des gens n'ont pas besoin de ce degré de portabilité.
Et à votre exemple sur la structure GUID: la structure de données affichée semble être la plupart du temps correcte, elle utilise des types de données qui sont garantis être suffisamment grands pour chacune des parties sur chaque plate-forme compatible ANSI. Donc, même si quelqu'un essaie d'utiliser cette structure pour écrire du code portable, ce serait parfaitement possible.
Comme vous l'avez remarqué par vous-même, si quelqu'un essayait d'utiliser cette structure comme spécification pour un GUID, il pourrait se plaindre du fait qu'elle est dans une certaine mesure imprécise et qu'elle nécessite la lecture complète de la documentation pour obtenir une spécification sans ambiguïté. C'est l'un des cas les moins fréquents où la taille maximale des types peut être importante.
D'autres problèmes peuvent survenir lorsque le contenu d'un tel struct
est au format chaîne, sérialisé binaire, stocké ou transmis quelque part, tout en faisant des hypothèses sur la taille maximale individuelle de chaque champ, ou la taille totale étant exactement de 128 bits, l'endianité, ou l'encodage binaire précis de ces types de données. Mais comme la documentation de la structure GUID ne fait aucune promesse concernant la représentation binaire sous-jacente, il ne faut pas faire d'hypothèses à ce sujet lorsque vous essayez d'écrire du code portable.
Ils ne sont pas dépréciés car il n'y a aucune raison de les déprécier.
Je suis presque tenté de m'en tenir à cela, car honnêtement, il n'y a pas grand-chose de plus à dire - les déprécier ne servirait à rien, alors personne n'a écrit un document pour les déprécier, et je ne peux pas vraiment imaginer quiconque prend la peine d'écrire un tel article non plus (sauf, je suppose, peut-être comme une blague du poisson d'avril, ou quelque chose sur cet ordre).
Mais considérons une utilisation typique de int
:
for (int i=0; i<10; i++)
std::cout << "something or other\n";
Maintenant, quelqu'un gagnerait-il quelque chose en changeant i
en int_fast8_t
, int_fast16_t
, ou quelque chose de similaire? Je suppose que la réponse est un "non" retentissant. Nous ne gagnerions pratiquement rien du tout.
Maintenant, il est certainement vrai qu'il existe des situations où il est logique d'utiliser des types de taille explicite tels que int8_t
, int16_t
et int32_t
(ou leurs variantes non signées).
Mais, une partie de l'intention de C et C++ est de prendre en charge la programmation système, et pour cela, il y a certainement des fois où je veux un type qui reflète la taille exacte d'un registre sur la machine cible. Étant donné qu'il s'agit d'une intention explicite à la fois de C et de C++, la dépréciation de types qui prennent en charge n'a aucun sens.
Ce qui se résume vraiment est assez simple: oui, il y a des cas où vous voulez un type qui est un nombre spécifique de bits - et si vous en avez besoin, C et C++ fournissent des types qui sont garantis exactement à la taille que vous spécifiez. Mais il y a aussi des cas où vous ne vous souciez pas beaucoup de la taille, tant qu'elle est suffisamment grande pour la plage que vous utilisez - et C et C++ fournissent également des types pour satisfaire ce besoin.
À partir de là, c'est à vous, le programmeur, de savoir ce que vous voulez vraiment et d'agir de manière appropriée. Oui, vous avez signalé un cas où quelqu'un (du moins sans doute) a fait un mauvais choix. Mais cela ne signifie pas que c'est toujours un mauvais choix, ni même nécessairement un mauvais choix la plupart du temps.
Une autre chose à garder à l'esprit est que, bien qu'il y ait des cas où la portabilité est importante, il y en a aussi beaucoup où cela importe peu, et d'autres où cela n'a pas d'importance du tout. Au moins d'après mon expérience, cependant, les tailles des types entiers sont rarement un facteur significatif de portabilité. D'une part, il est probablement vrai que si vous regardez beaucoup de code actuel, il y en a sans aucun doute beaucoup qui dépendent en fait que int
est d'au moins 32 bits, plutôt que les 16 bits spécifiés par les normes. Mais, si vous tentiez de porter la plupart de ce code vers (par exemple) un compilateur pour MS-DOS qui utilisait int
s 16 bits, vous rencontriez rapidement des problèmes beaucoup plus importants, tels que le fait qu'ils étaient utiliser ce int
pour indexer dans un tableau d'environ 10 millions double
s - et votre vrai problème de portage du code est beaucoup moins avec ce int
qu'avec le stockage de 80 millions octets sur un système qui ne prend en charge que 640 Ko.
Obsolète aujourd'hui signifie disparu demain.
Le coût de la suppression de ces types de C et C++ serait incroyablement élevé. Non seulement causant un travail inutile, mais aussi susceptible de causer des bugs partout.
La documentation de Microsoft pour GUID doit être lue conjointement avec les définitions spécifiques à la plate-forme compilateur C++ de Microsoft de ces valeurs, qui ont des tailles bien définies pour ces types, pas le Définition des normes ANSI C/C++. Dans un sens, les tailles de ces champs GUID sont bien définies dans les compilateurs de Microsoft.
L'en-tête GUID est bien sûr bogué sur les plates-formes non Microsoft, mais l'erreur ici est de penser que Microsoft se fout des implémentations standard et autres.
Le code C compilé (généralement) s’exécute en natif et la taille des mots natifs varie (ils étaient particulièrement variables au début des années 70 lorsque C a été développé pour la première fois). Vous avez toujours du code en cours d'exécution sur des machines 16 bits, des machines où la taille des mots ne sont pas des puissances de 2 (octets 9 bits, mots 36 bits), des machines qui utilisent des bits de remplissage, etc.
Chaque type garantit qu'il peut représenter un plage minimale de valeurs. int
est garanti pour représenter des valeurs dans au moins la plage [-32767..32767]
, ce qui signifie que c'est au moins 16 bits de large. Sur les ordinateurs de bureau et les serveurs modernes, il a généralement une largeur de 32 bits, mais ce n'est pas garanti.
Donc non, les largeurs de bits de char
, short
, int
, long
, etc., ne sont pas fixes, et c'est un bon chose du point de vue de C. C’est ce qui a permis à C d’être porté sur une telle variété de matériel.
C'est un peu comme parler.
Si vous vous parlez, peu importe la langue, les sons, etc. que vous utilisez, vous vous comprendrez probablement.
Si vous parlez à quelqu'un d'autre, il existe des règles spécifiques que doit être suivies afin que les deux parties comprennent clairement . La langue compte. Les règles grammaire pour la matière langue. Signification de phrases ou de mots spécifiques sont importants. Lorsque langue est écrit, la orthographe compte et la mise en page sur la page est importante.
Vous êtes libre de ne pas vous conformer aux règles et normes, mais les autres parties ne sont pas susceptibles de comprendre, et vous pouvez même causer des dommages en insultant ou en utilisant des phrases ambiguës. Des guerres ont été menées en raison d'échecs de communication.
Dans le logiciel, il existe des règles et des normes analogues.
Si le logiciel n'a pas besoin d'échanger des informations avec d'autres systèmes, alors oui, l'utilisation de court/long n'est pas nécessaire dans la plupart des cas tant que les données que vous traitez s'insèrent dans les conteneurs que vous définissez ou utilisez - un débordement est toujours possible.
Si - d'autre part - le logiciel échange des informations avec un autre système, alors ce logiciel doit être conscient de la façon dont cette information est structurée.
Par exemple:
Networking - les paquets absolument must ont un ordre d'octets correct - peu -endian vs big-endian - et les champs dans le paquet doit être le nombre correct de bits. Même lorsque vous pensez que vous envoyez des données `` évidentes '' comme JSON, ces données doivent être converties en paquets réseau qui peuvent être beaucoup plus courts que le total des données dans votre flux JSON, et les paquets ont également des champs pour le type de paquet, pour le séquençage - afin que vous puissiez réassembler les données du côté récepteur - pour la détection et la correction des erreurs, et bien beaucoup beaucoup plus. Tous les paquets réseau possibles doit être définis de telle manière qu'il ne puisse y avoir aucune ambiguïté sur l'expéditeur ou partie du récepteur. Pour que cela soit possible, vous doit pouvoir spécifier des tailles exactes pour les champs de paquets qui fonctionnent avec les systèmes et systèmes existants qui utilisera ces paquets à l'avenir.
Contrôle des périphériques - Très similaire à mise en réseau quand vous y pensez - - où les "champs" de paquets correspondent à peu près aux registres, bits, mémoire, etc. du périphérique, et le contrôle d'un périphérique spécifique correspond à peu près à l'utilisation d'un NIC ou l'adresse IP du réseau. Vous " envoyer "un" paquet "en écrivant des bits à des emplacements spécifiques, et vous" recevez "un" paquet "en lisant des bits à partir d'emplacements spécifiques. Si vous n'êtes pas le créateur de l'appareil - comme d'habitude - vous must suivez le 'protocole' énoncé par le créateur dans l'appareil fiche technique. Les champs (registres) ont la bonne taille. Les bits doivent être au bon endroit. Les registres doivent se trouvent correctement dans l'adresse du système ou dans l'espace d'E/S. Le créateur de l'appareil vous indique le "protocole" pour l'échange de données avec l'appareil. Le concepteur du système vous indique le "protocole" - espace d'adressage et mappage - pour accéder à l'appareil.
Vous êtes libre de faire ce que vous voulez dans le logiciel que vous écrivez, mais il est probable que l'autre partie - destinataire du réseau, appareil spécifique, etc. - ne comprendra pas ce que vous pensez faire, et dans certains cas, vous pouvez même endommager le système.
Le Ping-of-Death est un exemple de réseau où une violation intentionnelle du format de paquet a provoqué la panne de récepteurs réseau qui supposaient que les paquets réseau seraient correctement formés.
Le Fork-Bomb est un exemple de système où un abus intentionnel du "protocole" du fork du système peut bloquer un système jusqu'au redémarrage.
Buffer-Overrun est un exemple de programme où l'hypothèse "tout fonctionne" échoue lorsque quelqu'un (même vous-même en tant que programmeur) met trop de données dans un conteneur qui ne peut pas les contenir.