web-dev-qa-db-fra.com

Quel est ":-!!" en code C?

Je suis tombé sur cet étrange code de macro dans / usr/include/linux/kernel.h :

/* Force a compilation error if condition is true, but also produce a
   result (of value 0 and type size_t), so the expression can be used
   e.g. in a structure initializer (or where-ever else comma expressions
   aren't permitted). */
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))
#define BUILD_BUG_ON_NULL(e) ((void *)sizeof(struct { int:-!!(e); }))

Que fait :-!!?

1604
chmurli

C'est en effet un moyen de vérifier si l'expression e peut être évaluée à 0 et, si ce n'est pas le cas, échouer lors de la construction .

La macro est un peu mal nommée; cela devrait être quelque chose de plus semblable à BUILD_BUG_OR_ZERO, plutôt que ...ON_ZERO. (Il y a eu des discussions occasionnelles pour savoir s'il s'agit d'un nom déroutant.)

Vous devriez lire l'expression comme ceci:

sizeof(struct { int: -!!(e); }))
  1. (e): Expression de calcul e.

  2. !!(e): Nier logiquement deux fois: 0 if e == 0; sinon 1.

  3. -!!(e): Nier numériquement l'expression de l'étape 2: 0 s'il s'agissait de 0; sinon -1.

  4. struct{int: -!!(0);} --> struct{int: 0;}: S'il était égal à zéro, nous déclarons une structure avec un champ de bits entier anonyme de largeur zéro. Tout va bien et nous procédons comme d'habitude.

  5. struct{int: -!!(1);} --> struct{int: -1;}: D'autre part, si n'est pas zéro, alors ce sera un nombre négatif. Déclarer n'importe quel champ avec négatif width est une erreur de compilation.

Nous allons donc soit nous retrouver avec un champ de bits de largeur 0 dans une structure, ce qui est correct, soit avec un champ de bits de largeur négative, qui est une erreur de compilation. Ensuite, nous prenons sizeof ce champ, nous obtenons donc un size_t avec la largeur appropriée (qui sera zéro dans le cas où e est égal à zéro).


Certaines personnes ont demandé: Pourquoi ne pas simplement utiliser un assert?

la réponse de keithmo ici a une bonne réponse:

Ces macros implémentent un test à la compilation, tandis qu'assert () est un test à l'exécution.

Tout à fait raison. Vous ne voulez pas détecter dans votre noyau au moment de l'exécution des problèmes qui auraient pu être détectés auparavant! C'est un élément essentiel du système d'exploitation. Dans la mesure du possible, des problèmes peuvent être détectés au moment de la compilation, et tant mieux.

1635
John Feminella

Le : est un champ de bits. Quant à !!, il s'agit de double négation logique et renvoie donc 0 pour false ou 1 pour true. Et le - est un signe moins, c’est-à-dire une négation arithmétique.

C'est juste un truc pour que le compilateur arrête les entrées non valides.

Considérons BUILD_BUG_ON_ZERO. Lorsque -!!(e) renvoie une valeur négative, une erreur de compilation est générée. Sinon, -!!(e) est évalué à 0 et un champ de bits de largeur 0 a une taille de 0. Par conséquent, la macro est évaluée à un size_t avec la valeur 0.

Le nom est faible à mon avis car la construction échoue en fait lorsque l'entrée est et non zéro.

BUILD_BUG_ON_NULL est très similaire, mais produit un pointeur plutôt qu'un int.

250
David Heffernan

Certaines personnes semblent confondre ces macros avec assert().

Ces macros implémentent un test à la compilation, tandis que assert() est un test à l'exécution.

160
keithmo

Eh bien, je suis assez surpris que les alternatives à cette syntaxe n'aient pas été mentionnées. Un autre mécanisme courant (mais plus ancien) consiste à appeler une fonction non définie et à s’appuyer sur l’optimiseur pour extraire l’appel de fonction si votre assertion est correcte.

_#define MY_COMPILETIME_ASSERT(test)              \
    do {                                         \
        extern void you_did_something_bad(void); \
        if (!(test))                             \
            you_did_something_bad(void);         \
    } while (0)
_

Bien que ce mécanisme fonctionne (tant que les optimisations sont activées), il a l'inconvénient de ne pas signaler une erreur tant que vous n'avez pas établi de lien. À ce moment, il ne parvient pas à trouver la définition de la fonction you_did_something_bad (). C'est pourquoi les développeurs du noyau ont commencé à utiliser des astuces telles que les largeurs de champ de bits de taille négative et les tableaux de taille négative (le dernier d'entre eux ayant cessé de générer des constructions dans GCC 4.4).

Soucieux de la nécessité d’assertions à la compilation, GCC 4.3 a introduit l’attribut error] qui vous permet d’étendre ce concept plus ancien, mais de générer une erreur de compilation avec le message de votre choix - - plus de message d'erreur "tableau de taille négative" crypté!

_#define MAKE_SURE_THIS_IS_FIVE(number)                          \
    do {                                                        \
        extern void this_isnt_five(void) __attribute__((error(  \
                "I asked for five and you gave me " #number))); \
        if ((number) != 5)                                      \
            this_isnt_five();                                   \
    } while (0)
_

En fait, depuis Linux 3.9, nous avons maintenant une macro appelée compiletime_assert qui utilise cette fonctionnalité et la plupart des macros dans bug.h ont été mis à jour en conséquence. Néanmoins, cette macro ne peut pas être utilisée comme initialiseur. Cependant, en utilisant expressions d'expressions (une autre extension C de GCC), vous pouvez!

_#define ANY_NUMBER_BUT_FIVE(number)                           \
    ({                                                        \
        typeof(number) n = (number);                          \
        extern void this_number_is_five(void) __attribute__(( \
                error("I told you not to give me a five!"))); \
        if (n == 5)                                           \
            this_number_is_five();                            \
        n;                                                    \
    })
_

Cette macro évaluera son paramètre exactement une fois (au cas où elle aurait des effets secondaires) et créerait une erreur de compilation qui disait: "Je vous ai dit de ne pas me donner un cinq!" si l'expression évalue à cinq ou n'est pas une constante de compilation.

Alors, pourquoi n'utilisons-nous pas cela au lieu de champs de bits de taille négative? Hélas, il existe actuellement de nombreuses restrictions à l'utilisation d'expressions d'instructions, y compris leur utilisation en tant qu'initialiseurs constants (pour les constantes enum, la largeur du champ binaire, etc.), même si l'expression est totalement constante au moment de la compilation et passe le test __builtin_constant_p() ). De plus, ils ne peuvent pas être utilisés en dehors d'un corps de fonction.

Espérons que GCC corrigera bientôt ces lacunes et permettra l'utilisation d'expressions d'instructions constantes en tant qu'initialiseurs constants. Le défi ici est la spécification du langage définissant ce qu'est une expression constante légale. C++ 11 a ajouté le mot clé constexpr pour ce type ou cette chose, mais il n’existe aucune contrepartie dans C11. Bien que C11 reçoive des assertions statiques, ce qui résoudra une partie de ce problème, il ne résoudra pas tous ces problèmes. J'espère donc que gcc pourra rendre une fonctionnalité constexpr disponible sous la forme d'une extension via -std = gnuc99 & -std = gnuc11 ou une autre, et autorisera son utilisation dans les expressions d'instructions et. Al.

48
Daniel Santos

Il crée un champ de bits 0 si la condition est fausse, mais un champ de bits -1 (-!!1) si la condition est vraie/non nulle. Dans le premier cas, il n'y a pas d'erreur et la structure est initialisée avec un membre int. Dans ce dernier cas, il y a une erreur de compilation (et aucun champ de taille -1 bitfield n'est créé, bien sûr).

34
Matt Phillips