Le contexte
Nous portons du code C qui a été initialement compilé à l'aide d'un compilateur C 8 bits pour le microcontrôleur PIC. Un idiome commun utilisé pour empêcher les variables globales non signées (par exemple, les compteurs d'erreurs) de revenir à zéro est le suivant:
if(~counter) counter++;
L'opérateur au niveau du bit inverse ici tous les bits et l'instruction n'est vraie que si elle counter
est inférieure à la valeur maximale. Surtout, cela fonctionne indépendamment de la taille variable.
Problème
Nous visons maintenant un processeur ARM 32 bits utilisant GCC. Nous avons remarqué que le même code produit des résultats différents. Pour autant que nous puissions en juger, il semble que l'opération de complément au niveau du bit renvoie une valeur d'une taille différente de celle à laquelle nous nous attendions. Pour reproduire cela, nous compilons, dans GCC:
uint8_t i = 0;
int sz;
sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1
sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4
Dans la première ligne de sortie, nous obtenons ce que nous attendons: i
est de 1 octet. Cependant, le complément au niveau du bit de i
est en fait de quatre octets, ce qui cause un problème car les comparaisons avec celui-ci ne donneront pas les résultats attendus. Par exemple, si vous faites (où i
est correctement initialisé uint8_t
):
if(~i) i++;
Nous verrons i
"boucler" de 0xFF à 0x00. Ce comportement est différent dans GCC par rapport à quand il fonctionnait comme nous le voulions dans le compilateur précédent et le microcontrôleur PIC 8 bits.
Nous sommes conscients que nous pouvons résoudre ce problème en effectuant un casting comme ceci:
if((uint8_t)~i) i++;
Ou, par
if(i < 0xFF) i++;
Toutefois, dans ces deux solutions de contournement, la taille de la variable doit être connue et est sujette à erreur pour le développeur de logiciels. Ces types de vérification des limites supérieures se produisent dans toute la base de code. Il y a plusieurs tailles de variables (par exemple, uint16_t
et unsigned char
etc.) et les changer dans une base de code qui fonctionne autrement n'est pas quelque chose que nous attendons avec impatience.
Question
Notre compréhension du problème est-elle correcte et existe-t-il des options pour résoudre ce problème qui ne nécessitent pas de revoir chaque cas où nous avons utilisé cet idiome? Notre hypothèse est-elle correcte, qu'une opération comme le complément au niveau du bit devrait retourner un résultat de la même taille que l'opérande? Il semble que cela se casserait, selon les architectures du processeur. J'ai l'impression de prendre des pilules folles et que C devrait être un peu plus portable que ça. Encore une fois, notre compréhension de cela pourrait être fausse.
En apparence, cela ne semble pas être un problème énorme, mais cet idiome fonctionnant précédemment est utilisé dans des centaines d'endroits et nous sommes impatients de le comprendre avant de procéder à des changements coûteux.
Remarque: Il y a une question en double apparemment similaire mais pas exacte ici: l' opération au niveau du bit sur char donne un résultat 32 bits
Je n'ai pas vu le véritable nœud du problème discuté ici, à savoir, la taille du résultat d'un complément au niveau du bit étant différente de celle transmise à l'opérateur.
la source
Réponses:
Ce que vous voyez est le résultat de promotions entières . Dans la plupart des cas où une valeur entière est utilisée dans une expression, si le type de la valeur est plus petit que
int
la valeur est promueint
. Ceci est documenté dans la section 6.3.1.1p2 de la norme C :Donc, si une variable a un type
uint8_t
et la valeur 255, l'utilisation d'un opérateur autre qu'une conversion ou une affectation dessus la convertira d'abord en typeint
avec la valeur 255 avant d'effectuer l'opération. C'est pourquoisizeof(~i)
vous donne 4 au lieu de 1.La section 6.5.3.3 décrit que les promotions entières s'appliquent à l'
~
opérateur:Donc, en supposant un 32 bits
int
, sicounter
a la valeur 8 bits,0xff
il est converti en valeur 32 bits0x000000ff
, et l'appliquer~
vous donne0xffffff00
.La façon la plus simple de gérer cela est probablement sans avoir à connaître le type de vérifier si la valeur est 0 après l'incrémentation, et si c'est le cas décrémenter.
L'enveloppement d'entiers non signés fonctionne dans les deux sens, donc la décrémentation d'une valeur de 0 vous donne la plus grande valeur positive.
la source
if (!++counter) --counter;
pourrait être moins bizarre pour certains programmeurs que d'utiliser l'opérateur virgule.++counter; counter -= !counter;
.increment_unsigned_without_wraparound
ouincrement_with_saturation
. Personnellement, j'utiliserais une fonction générique à trois opérandesclamp
.en taille de (i); vous demandez la taille de la variable i , donc 1
en taille de (~ i); vous demandez la taille du type de l'expression, qui est un int , dans votre cas 4
Utiliser
savoir si je ne valorise pas 255 (dans votre cas avec un uint8_t) n'est pas très lisible, il suffit de faire
et vous aurez un code portable et lisible
Pour gérer n'importe quelle taille de non signé:
L'expression est constante, donc calculée au moment de la compilation.
#include <limits.h> pour CHAR_BIT et #include <stdint.h> pour uintmax_t
la source
!= 255
est donc inadéquate.unsigned
objets car les décalages de la largeur totale de l'objet ne sont pas définis par la norme C, mais cela peut être corrigé avec(2u << sizeof(i)*CHAR_BIT-1) - 1
.((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1
.Voici plusieurs options pour implémenter «Ajouter 1 à
x
mais fixer à la valeur représentable maximale», étant donné qu'ilx
s'agit d'un type entier non signé:Ajoutez-en un si et seulement si
x
est inférieur à la valeur maximale représentable dans son type:Voir l'article suivant pour la définition de
Maximum
. Cette méthode a de bonnes chances d'être optimisée par un compilateur pour des instructions efficaces telles qu'une comparaison, une certaine forme d'ensemble ou de déplacement conditionnel et un ajout.Comparez à la plus grande valeur du type:
(Ceci calcule 2 N , où N est le nombre de bits en
x
, en décalant 2 de N -1 bits. Nous faisons cela au lieu de décaler 1 N bits car un décalage du nombre de bits dans un type n'est pas défini par le C LaCHAR_BIT
macro peut ne pas être familière à certains; c'est le nombre de bits dans un octet, toutsizeof x * CHAR_BIT
comme le nombre de bits dans le type dex
.)Cela peut être enveloppé dans une macro comme souhaité pour l'esthétique et la clarté:
Incrémentez
x
et corrigez s'il revient à zéro, à l'aide d'unif
:Incrémentez
x
et corrigez s'il revient à zéro, en utilisant une expression:Ceci est nominalement sans branche (parfois bénéfique pour les performances), mais un compilateur peut l'implémenter comme ci-dessus, en utilisant une branche si nécessaire mais éventuellement avec des instructions inconditionnelles si l'architecture cible a des instructions appropriées.
Une option sans branche, utilisant la macro ci-dessus, est:
Si
x
est le maximum de son type, ceci est évalué àx += 1-1
. Sinon, ça l'estx += 1-0
. Cependant, la division est quelque peu lente sur de nombreuses architectures. Un compilateur peut optimiser cela en instructions sans division, selon le compilateur et l'architecture cible.la source
-Wshift-op-parentheses
. La bonne nouvelle est qu'un compilateur d'optimisation ne va pas générer de division ici, vous n'avez donc pas à vous soucier de sa lenteur.sizeof x
ne peut pas être implémenté à l'intérieur d'une fonction C car lex
devrait être un paramètre (ou une autre expression) avec un type fixe. Il ne pouvait pas produire la taille du type d'argument utilisé par l'appelant. Une macro peut.Avant stdint.h, les tailles des variables peuvent varier d'un compilateur à l'autre et les types de variables réels en C sont toujours int, longs, etc. et sont toujours définis par l'auteur du compilateur quant à leur taille. Pas d'hypothèses standard ou spécifiques. Les auteurs doivent ensuite créer stdint.h pour mapper les deux mondes, c'est le but de stdint.h pour mapper uint_this celui en int, long, short.
Si vous portez du code à partir d'un autre compilateur et qu'il utilise char, short, int, long, vous devez passer par chaque type et faire le port vous-même, il n'y a aucun moyen de le contourner. Et soit vous vous retrouvez avec la bonne taille pour la variable, la déclaration change mais le code écrit fonctionne ....
ou ... fournissez le masque ou le transtypage directement
À la fin de la journée, si vous voulez que ce code fonctionne, vous devez le porter sur la nouvelle plate-forme. Votre choix quant à la manière. Oui, vous devez passer du temps à chaque cas et le faire correctement, sinon vous allez continuer à revenir à ce code qui est encore plus cher.
Si vous isolez les types de variables sur le code avant le portage et quelle est la taille des types de variables, puis isolez les variables qui le font (devrait être facile à grep) et modifiez leurs déclarations à l'aide des définitions stdint.h qui, espérons-le, ne changeront pas à l'avenir, et vous seriez surpris, mais les mauvais en-têtes sont parfois utilisés, alors même mettez des chèques pour que vous puissiez mieux dormir la nuit
Et bien que ce style de codage fonctionne (si (~ counter) counter ++;), pour la portabilité le souhaite maintenant et à l'avenir, il est préférable d'utiliser un masque pour limiter spécifiquement la taille (et ne pas s'appuyer sur la déclaration), faites-le lorsque le le code est écrit en premier lieu ou il suffit de terminer le port et vous n'aurez pas à le re-porter à nouveau un autre jour. Ou pour rendre le code plus lisible, faites le si x <0xFF alors ou x! = 0xFF ou quelque chose comme ça alors le compilateur peut l'optimiser dans le même code qu'il le ferait pour n'importe laquelle de ces solutions, le rend simplement plus lisible et moins risqué ...
Cela dépend de l'importance du produit ou du nombre de fois que vous souhaitez envoyer des correctifs / mises à jour ou faire rouler un camion ou vous rendre au laboratoire pour régler la question de savoir si vous essayez de trouver une solution rapide ou simplement de toucher les lignes de code affectées. si ce n'est qu'une centaine ou quelques-uns, ce n'est pas si grand qu'un port.
la source
C 2011 Draft en ligne
Le problème est que l'opérande de
~
est promuint
avant que l'opérateur ne soit appliqué.Malheureusement, je ne pense pas qu'il existe un moyen facile de résoudre ce problème. L'écriture
n'aidera pas, car les promotions s'appliquent également là-bas. La seule chose que je peux suggérer est de créer des constantes symboliques pour la valeur maximale que vous souhaitez que cet objet représente et de tester par rapport à cela:
la source
-1
convient toutefois de noter que dans votre deuxième exemple de code, le n'est pas nécessaire, car cela entraînerait le compteur à 254 (0xFE). Dans tous les cas, cette approche, comme mentionné dans ma question, n'est pas idéale en raison des différentes tailles de variables dans la base de code qui participent à cet idiome.