Certaines personnes semblent confondre ces macros avec assert()
.
Ces macros implémentent un test de compilation, tandis que assert()
est un test d'exécution.
C'est, en effet, un moyen de vérifier si l'expression e peut être évaluée comme étant 0, et si ce n'est pas le cas, de faire échouer la construction .
La macro est quelque peu mal nommée ; ça devrait être quelque chose comme 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); }))
-
(e)
:Calcule l'expressione
. -
!!(e)
:Inverse logiquement deux fois :0
sie == 0
; sinon1
. -
-!!(e)
:Annulez numériquement l'expression de l'étape 2 :0
si c'était0
; sinon-1
. -
struct{int: -!!(0);} --> struct{int: 0;}
:Si c'était zéro, alors nous déclarons une structure avec un champ de bits entier anonyme qui a une largeur nulle. Tout va bien et nous procédons normalement. -
struct{int: -!!(1);} --> struct{int: -1;}
:En revanche, si ce n'est pas le cas zéro, alors ce sera un nombre négatif. Déclarer n'importe quel champ de bits avec négatif largeur est une erreur de compilation.
Nous allons donc soit nous retrouver avec un champ de bits qui a une largeur de 0 dans une structure, ce qui est bien, soit un champ de bits avec une largeur négative, ce 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 nulle dans le cas où e
est 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 de compilation, tandis que assert() est un test d'exécution.
Exactement. Vous ne voulez pas détecter de problèmes dans votre noyau à l'exécution qui aurait pu être détecté plus tôt ! C'est un élément essentiel du système d'exploitation. Dans la mesure où les problèmes peuvent être détectés au moment de la compilation, tant mieux.
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 qui n'est pas définie et à compter sur l'optimiseur pour compiler 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 d'erreur jusqu'à ce que vous liiez, auquel cas 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 ayant cessé de casser les builds dans GCC 4.4).
Par sympathie pour le besoin d'assertions au moment de la compilation, GCC 4.3 a introduit le error
attribut de fonction qui vous permet d'étendre ce concept plus ancien, mais génère une erreur de compilation avec un message de votre choix -- plus de messages d'erreur cryptiques "tableau de taille négative" !
#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, l'utilisation d'expressions d'instruction (une autre extension GCC C), 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 indiquant "Je t'avais dit de ne pas me donner un cinq !" si l'expression vaut 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 des expressions d'instruction, y compris leur utilisation en tant qu'initialiseurs constants (pour les constantes enum, la largeur du champ de bits, etc.) même si l'expression d'instruction est complètement constante (c'est-à-dire qu'elle peut être entièrement évaluée au moment de la compilation et sinon passe le __builtin_constant_p()
test). De plus, ils ne peuvent pas être utilisés en dehors du corps d'une fonction.
Espérons que GCC corrigera bientôt ces lacunes et autorisera l'utilisation d'expressions d'instructions constantes comme 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 uniquement pour ce type ou cette chose, mais aucun équivalent n'existe en C11. Bien que C11 ait obtenu des assertions statiques, ce qui résoudra une partie de ce problème, il ne résoudra pas toutes ces lacunes. J'espère donc que gcc pourra rendre une fonctionnalité constexpr disponible en tant qu'extension via -std=gnuc99 &-std=gnuc11 ou une autre et permettre son utilisation sur les expressions d'instruction et. al.
Le :
est un champ de bits. Comme pour !!
, c'est une double négation logique et renvoie donc 0
pour faux ou 1
Pour de vrai. Et le -
est un signe moins, c'est-à-dire une négation arithmétique.
Ce n'est qu'une astuce pour que le compilateur vomisse sur des entrées invalides.
Considérez BUILD_BUG_ON_ZERO
. Quand -!!(e)
évalue à une valeur négative, qui produit une erreur de compilation. Sinon -!!(e)
évalue à 0, et un champ de bits de largeur 0 a une taille de 0. Et donc la macro évalue à un size_t
avec la valeur 0.
Le nom est faible à mon avis car la construction échoue en fait lorsque l'entrée n'est pas zéro.
BUILD_BUG_ON_NULL
est très similaire, mais donne un pointeur plutôt qu'un int
.