Pour le code intégré, pourquoi devrais-je utiliser les types «uint_t» au lieu de «unsigned int»?

22

J'écris une application en c pour un STM32F105, en utilisant gcc.

Dans le passé (avec des projets plus simples), je l' ai toujours des variables définies comme char, int, unsigned intet ainsi de suite.

Je vois qu'il est courant d'utiliser les types définis dans stdint.h, tels que int8_t, uint8_t, uint32_t, etc. il vrai dans plusieurs API que j'utilise, et aussi dans la bibliothèque ARM CMSIS de ST.

Je crois que je comprends pourquoi nous devrions le faire; pour permettre au compilateur de mieux optimiser l'espace mémoire. Je pense qu'il peut y avoir des raisons supplémentaires.

Cependant, en raison des règles de promotion des entiers de c, je continue de me heurter aux avertissements de conversion chaque fois que j'essaie d'ajouter deux valeurs, d'effectuer une opération au niveau du bit, etc. L'avertissement se lit comme suit conversion to 'uint16_t' from 'int' may alter its value [-Wconversion]. La question est discutée ici et ici .

Cela ne se produit pas lors de l'utilisation de variables déclarées comme intou unsigned int.

Pour donner quelques exemples, étant donné ceci:

uint16_t value16;
uint8_t value8;

Je devrais changer cela:

value16 <<= 8;
value8 += 2;

pour ça:

value16 = (uint16_t)(value16 << 8);
value8 = (uint8_t)(value8 + 2);

C'est moche, mais je peux le faire si nécessaire. Voici mes questions:

  1. Existe-t-il un cas où la conversion de non signé en signé et de retour en non signé rendra le résultat incorrect?

  2. Y a-t-il d'autres grandes raisons pour / contre l'utilisation des types entiers stdint.h?

Sur la base des réponses que je reçois, il semble que les types stdint.h soient généralement préférés, même si c se convertit en uintva- intet-vient. Cela conduit à une plus grande question:

  1. Je peux empêcher les avertissements du compilateur en utilisant le transtypage (par exemple value16 = (uint16_t)(value16 << 8);). Suis-je juste en train de cacher le problème? Y a-t-il une meilleure façon de procéder?
bitsmack
la source
Utilisez des littéraux non signés: ie 8uet 2u.
Arrêtez de nuire à Monica
Merci, @OrangeDog, je pense que je me méprends. J'ai essayé les deux value8 += 2u;et value8 = value8 + 2u;, mais je reçois les mêmes avertissements.
bitsmack
Utilisez-les quand même pour éviter les avertissements signés lorsque vous n'avez pas encore d'avertissements de largeur :)
Arrêtez de nuire à Monica

Réponses:

11

Un compilateur conforme aux normes où se intsituait entre 17 et 32 ​​bits peut légitimement faire tout ce qu'il veut avec le code suivant:

uint16_t x = 46341;
uint32_t y = x*x; // temp result is signed int, which can't hold 2147488281

Une implémentation qui le ferait pourrait légitimement générer un programme qui ne ferait rien d'autre que de sortir la chaîne "Fred" à plusieurs reprises sur chaque broche de port en utilisant tous les protocoles imaginables. La probabilité qu'un programme soit porté sur une implémentation qui ferait une telle chose est exceptionnellement faible, mais elle est théoriquement possible. Si vous souhaitez écrire le code ci-dessus afin qu'il soit garanti de ne pas s'engager dans un comportement indéfini, il serait nécessaire d'écrire cette dernière expression sous la forme (uint32_t)x*xou 1u*x*x. Sur un compilateur où se intsitue entre 17 et 31 bits, cette dernière expression tronquerait les bits supérieurs, mais ne s'engagerait pas dans un comportement indéfini.

Je pense que les avertissements gcc essaient probablement de suggérer que le code tel qu'il est écrit n'est pas complètement 100% portable. Il y a des moments où le code doit vraiment être écrit pour éviter les comportements qui ne seraient pas définis sur certaines implémentations, mais dans de nombreux autres cas, il faut simplement comprendre qu'il est peu probable que le code soit utilisé sur des implémentations qui feraient des choses trop ennuyeuses.

Notez que l'utilisation de types tels que intet shortpeut éliminer certains avertissements et résoudre certains problèmes, mais en créerait probablement d'autres. L'interaction entre des types comme uint16_tet les règles de promotion des nombres entiers de C est épineuse, mais ces types sont probablement encore meilleurs que n'importe quelle alternative.

supercat
la source
6

1) Si vous effectuez simplement un cast entre un entier non signé et un entier signé de la même longueur, sans aucune opération entre les deux, vous obtiendrez le même résultat à chaque fois, donc pas de problème ici. Mais diverses opérations logiques et arithmétiques agissent différemment sur les opérandes signés et non signés.
2) La principale raison d'utiliser des stdint.htypes est que la taille en bits d'un tel type est définie et égale sur toutes les plates-formes, ce qui n'est pas vrai pour int, longetc., et charn'a pas de signe standard, il peut être signé ou non signé par défaut. Il facilite la manipulation des données en connaissant la taille exacte sans recourir à des vérifications et hypothèses supplémentaires.

Eugene Sh.
la source
2
Les tailles de int32_tet uint32_tsont égales sur toutes les plateformes sur lesquelles elles sont définies . Si le processeur n'a pas de type matériel correspondant exactement , ces types ne sont pas définis. D'où l'avantage de intetc. et, peut-être, int_least32_tetc.
Pete Becker
1
@PeteBecker - C'est sans doute un avantage, car les erreurs de compilation qui en résultent vous rendent immédiatement conscient du problème. Je préfère de beaucoup que mes types changent de taille sur moi.
sapi
@sapi - dans de nombreuses situations, la taille sous-jacente n'est pas pertinente; Les programmeurs C s'entendaient très bien sans tailles fixes pendant de nombreuses années.
Pete Becker
6

Étant donné que le # 2 d'Eugène est probablement le point le plus important, je voudrais juste ajouter que c'est un avis

MISRA (directive 4.6): "typedefs that indicate size and signedness should be used in place of the basic types".

Jack Ganssle semble également être partisan de cette règle: http://www.ganssle.com/tem/tem265.html

Tom L.
la source
2
Il est dommage qu'il n'y ait aucun type pour spécifier "un entier non signé de N bits qui peut être multiplié en toute sécurité par n'importe quel autre entier de même taille pour donner le même résultat". Les règles de promotion entières interagissent horriblement avec les types existants comme uint32_t.
supercat
3

Un moyen simple d'éliminer les avertissements est d'éviter d'utiliser -Wconversion dans GCC. Je pense que vous devez activer cette option manuellement, mais sinon, vous pouvez utiliser -Wno-conversion pour la désactiver. Vous pouvez activer les avertissements pour les conversions de signes et de précision FP via d' autres options , si vous les souhaitez toujours.

Les avertissements de -Wconversion sont presque toujours de faux positifs, ce qui explique probablement pourquoi même -Wextra ne le permet pas par défaut. Une question Stack Overflow a beaucoup de suggestions pour de bons jeux d'options. D'après ma propre expérience, c'est un bon point de départ:

-std = c99 -pédantique -Mur -Wextra -Wshadow

Ajoutez-en plus si vous en avez besoin, mais il y a de fortes chances que vous ne le fassiez pas.

Si vous devez conserver -Wconversion, vous pouvez raccourcir un peu votre code en ne transtypant que l'opérande numérique:

value16 <<= (uint16_t)8;
value8 += (uint8_t)2;

Ce n'est pas facile à lire sans mettre en évidence la syntaxe.

Adam Haun
la source
2

dans tout projet logiciel, il est très important d'utiliser des définitions de type portables. (même la prochaine version du même compilateur a besoin de cette considération.) Un bon exemple, il y a plusieurs années, j'ai travaillé sur un projet où le compilateur actuel définissait 'int' comme 8bits. La prochaine version du compilateur a défini «int» comme 16 bits. Comme nous n'avions pas utilisé de définitions portables pour «int», le ram a (effectivement) doublé de taille et de nombreuses séquences de code qui dépendaient d'un int 8 bits ont échoué. L'utilisation d'une définition de type portable aurait évité ce problème (des centaines d'heures de travail à résoudre).

Richard Williams
la source
Aucun code raisonnable ne doit utiliser intpour faire référence à un type 8 bits. Même si un compilateur non-C comme CCS le fait, un code raisonnable doit utiliser soit charun type typedefed pour 8 bits, et un type typedefed (pas "long") pour 16 bits. D'un autre côté, le portage de code de quelque chose comme CCS vers un vrai compilateur est susceptible d'être problématique même s'il utilise des typedefs appropriés, car de tels compilateurs sont souvent "inhabituels" à d'autres égards.
supercat
1
  1. Oui. Un entier signé de n bits peut représenter environ la moitié du nombre de nombres non négatifs en tant qu'entier non signé de n bits, et s'appuyer sur des caractéristiques de débordement est un comportement non défini pour que tout puisse arriver. La grande majorité des processeurs actuels et passés utilisent un complément à deux, de sorte que de nombreuses opérations font la même chose sur les types intégraux signés et non signés, mais même dans ce cas, toutes les opérations ne produiront pas des résultats identiques au niveau du bit. Vous demandez vraiment des problèmes supplémentaires plus tard lorsque vous ne pouvez pas comprendre pourquoi votre code ne fonctionne pas comme prévu.

  2. Alors que int et unsigned ont des tailles définies par l'implémentation, celles-ci sont souvent choisies "intelligemment" par l'implémentation pour des raisons de taille ou de vitesse. Je m'y tiens généralement, sauf si j'ai une bonne raison de faire autrement. De même, lorsque je décide d'utiliser int ou unsigned, je préfère généralement int sauf si j'ai une bonne raison de le faire autrement.

Dans les cas où j'ai vraiment besoin d'un meilleur contrôle de la taille ou de la signature d'un type, je préfère généralement utiliser un typedef défini par le système (size_t, intmax_t, etc.) ou créer mon propre typedef qui indique la fonction d'un élément donné type (prng_int, adc_int, etc.).

helloworld922
la source
0

Souvent, le code est utilisé sur ARM thumb et AVR (et x86, powerPC et autres architectures), et 16 ou 32 bits peuvent être plus efficaces (dans les deux sens: flash et cycles) sur STM32 ARM même pour une variable qui tient en 8 bits ( 8 bits est plus efficace sur AVR) . Cependant, si la SRAM est presque pleine, le retour à 8 bits pour les variables globales peut être raisonnable (mais pas pour les variables locales). Pour la portabilité et la maintenance (en particulier pour les vars 8 bits), il a des avantages (sans aucun inconvénient) de spécifier la taille MINIMALE appropriée , au lieu de la taille exacte et typedef à un endroit .h (généralement sous ifdef) à régler (éventuellement uint_fast8_t / uint_least8_t) pendant le portage / la construction, par exemple:

// apparently uint16_t is just as efficient as 32 bit on STM32, but 8 bit is punished (with more flash and cycles)
typedef uint16_t uintG8_t; // 8bit if SRAM is scarce (use fol global vars that fit in 8 bit)
typedef uint16_t uintL8_t; // 8bit on AVR (local var, 16 or 32 bit is more efficient on STM + less flash)
// might better reserve 32 bits on some arch, STM32 seems efficient with 16 bits:
typedef uint16_t uintG16_t; // 16bit if SRAM is scarce (use fol global vars that fit in 16 bit)
typedef uint16_t uintL16_t; // 16bit on AVR (local var, 16 or 32 bit whichever is more efficient on other arch)

La bibliothèque GNU aide un peu, mais normalement les typedefs ont quand même un sens:

typedef uint_least8_t uintG8_t;
typedef uint_fast8_t uintL8_t;

// mais uint_fast8_t pour BOTH lorsque SRAM n'est pas un problème.

Marcell
la source