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

1665

J'ai rencontré 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 :-!!-il?

chmurli
la source
2
- Unaire moins <br />! NON logique <br /> inverse non pas de l'entier e donné, la variable peut donc être 0 ou 1.
CyrillC
69
git blame nous dit que cette forme particulière d'affirmation statique a été introduite par Jan Beulich dans 8c87df4 . De toute évidence, il avait de bonnes raisons de le faire (voir le message de validation).
Niklas
55
@Lundin: assert () ne provoque PAS d'erreur de compilation. C'est tout l'intérêt de la construction ci-dessus.
Chris Pacejo
4
@GreweKokkor Ne soyez pas naïf, Linux est trop gros pour qu'une seule personne puisse tout gérer. Linus a ses liutenants et ils ont le leur qui pousse les changements et les améliorations de bas en haut. Linus décide seulement s'il veut une fonctionnalité ou non, mais il fait confiance à ses collègues dans une certaine mesure. Si vous voulez en savoir plus sur le fonctionnement du système distribué dans un environnement open source, consultez la vidéo youtube: youtube.com/watch?v=4XpnKHJAok8 (C'est plutôt une discussion très intéressante).
Tomas Pruzina
3
@cpcloud, sizeof"évalue" le type, mais pas la valeur. C'est le type qui n'est pas valide dans ce cas.
Winston Ewert

Réponses:

1692

C'est, en effet, un moyen de vérifier si l'expression e peut être évaluée à 0, et sinon, d'échouer la construction .

La macro est quelque peu mal nommée; ce devrait être quelque chose de plus 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): Calculer l'expression e.

  2. !!(e): Niez logiquement deux fois: 0si e == 0; sinon 1.

  3. -!!(e): Nie numériquement l'expression de l'étape 2: 0si elle l'était 0; sinon -1.

  4. 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.

  5. struct{int: -!!(1);} --> struct{int: -1;}: D'un autre côté, si ce n'est pas zéro, ce sera un nombre négatif. La déclaration de tout champ binaire de largeur négative est une erreur de compilation.

Nous allons donc soit nous retrouver avec un champ de bits qui a une largeur 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 sizeofce champ, nous obtenons donc un size_tavec la largeur appropriée (qui sera zéro dans le cas où eest 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 au moment de la compilation, tandis que assert () est un test au moment de l'exécution.

Exactement raison. Vous ne voulez pas détecter lors de l'exécution des problèmes dans votre noyau qui auraient pu être détectés plus tôt! C'est un élément essentiel du système d'exploitation. Dans toute la mesure du possible, des problèmes peuvent être détectés au moment de la compilation.

John Feminella
la source
5
@weston Beaucoup d'endroits différents. Voir par vous-même!
John Feminella
166
les variantes récentes des normes C ++ ou C ont quelque chose comme static_assertà des fins connexes.
Basile Starynkevitch du
54
@Lundin - #error nécessiterait l'utilisation de 3 lignes de code # if / # error / # endif, et ne fonctionnerait que pour les évaluations accessibles au pré-processeur. Ce hack fonctionne pour toute évaluation accessible au compilateur.
Ed Staub
236
Le noyau Linux n'utilise pas C ++, du moins pas tant que Linus est toujours en vie.
Mark Ransom
6
@ Dolda2000: " Les expressions booléennes en C sont définies pour toujours être évaluées à zéro ou à un " - Pas exactement. Les opérateurs qui donnent des résultats « logiquement » booléennes ( !, <, >, <=, >=, ==, !=, &&, ||) Cédez toujours 0 ou 1. D' autres expressions peuvent donner des résultats qui peuvent être utilisés en tant que conditions, mais sont simplement nul ou non nul; par exemple, isdigit(c)cest un chiffre, peut donner n'importe quelle valeur non nulle (qui est alors traitée comme vraie dans une condition).
Keith Thompson
256

C'est :un champ de bits. Quant à !!, c'est une double négation logique et renvoie donc 0pour faux ou 1pour vrai. Et le -est un signe moins, c'est-à-dire la négation arithmétique.

C'est juste une astuce pour amener le compilateur à barf sur les entrées invalides.

Considérez BUILD_BUG_ON_ZERO. Lorsqu'évalue -!!(e)à une valeur négative, cela produit une erreur de compilation. Sinon, il est -!!(e)é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_tavec une valeur 0.

Le nom est faible à mon avis car la construction échoue en fait lorsque l'entrée n'est pas nulle.

BUILD_BUG_ON_NULLest très similaire, mais donne un pointeur plutôt qu'un int.

David Heffernan
la source
14
est sizeof(struct { int:0; })strictement conforme?
ouah
7
Pourquoi le résultat serait-il en général 0? Un structavec seulement un champ de bits vide, c'est vrai, mais je ne pense pas que les structures de taille 0 soient autorisées. Par exemple, si vous créez un tableau de ce type, les éléments de tableau individuels doivent toujours avoir des adresses différentes, non?
Jens Gustedt
2
ils ne s'en soucient pas car ils utilisent des extensions GNU, ils désactivent la règle d'aliasing stricte et ne considèrent pas les débordements d'entier comme UB. Mais je me demandais si c'était strictement conforme C.
ouah
3
@ouah concernant les champs de bits de longueur nulle sans nom, voir ici: stackoverflow.com/questions/4297095/…
David Heffernan
9
@DavidHeffernan C permet en fait un champ de bits 0non nommé de largeur, mais pas s'il n'y a aucun autre membre nommé dans la structure. (C99, 6.7.2.1p2) "If the struct-declaration-list contains no named members, the behavior is undefined."Ainsi, par exemple, il sizeof (struct {int a:1; int:0;})est strictement conforme mais sizeof(struct { int:0; })ne l'est pas (comportement indéfini).
ouah
168

Certaines personnes semblent confondre ces macros avec assert().

Ces macros implémentent un test au moment de la compilation, alors qu'il assert()s'agit d'un test d'exécution.

Keithmo
la source
52

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 à s'appuyer 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 établissiez un lien, auquel cas il ne trouve pas la définition de la fonction you_did_something_bad (). C'est pourquoi les développeurs du noyau commencent à utiliser des astuces comme les largeurs de champ de bits de taille négative et les tableaux de taille négative (dont le dernier a 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 l' errorattribut de fonction qui vous permet d'étendre sur ce concept plus ancien, mais de générer une erreur au moment de la compilation avec un message de votre choix - plus de tableau de taille négative "plus cryptique" " messages d'erreur!

#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_assertqui utilise cette fonctionnalité et la plupart des macros dans bug.hont été mises à jour en conséquence. Pourtant, cette macro ne peut pas être utilisée comme initialiseur. Cependant, en utilisant des expressions par instruction (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éera une erreur de compilation qui dit "Je vous ai dit de ne pas m'en donner cinq!" si l'expression est évaluée à cinq ou n'est pas une constante de temps de compilation.

Alors pourquoi n'utilisons-nous pas cela au lieu de champs binaires de taille négative? Hélas, il existe actuellement de nombreuses restrictions à l'utilisation des expressions d'instruction, y compris leur utilisation comme initialiseurs constants (pour les constantes d'énumération, la largeur du champ de bits, etc.) même si l'expression d'instruction est complètement constante elle-même (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 d'un corps de fonction.

Avec un peu de chance, GCC modifiera bientôt ces lacunes et permettra d'utiliser des expressions d'instruction 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 pour ce type ou cette chose, mais aucune contrepartie 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 tous ces défauts. J'espère donc que gcc pourra rendre une fonctionnalité constexpr disponible en tant qu'extension via -std = gnuc99 & -std = gnuc11 ou quelque chose du genre et permettre son utilisation sur les expressions d'instructions et. Al.

Daniel Santos
la source
6
Toutes vos solutions ne sont PAS des alternatives. Le commentaire au-dessus de la macro est assez clair " so the expression can be used e.g. in a structure initializer (or where-ever else comma expressions aren't permitted)." La macro renvoie une expression de typesize_t
Wiz
3
@Wiz Oui, j'en suis conscient. Peut-être que c'était un peu bavard et que je devais peut-être revoir ma formulation, mais mon but était d'explorer les différents mécanismes pour les assertions statiques et de montrer pourquoi nous utilisons toujours des champs de bits de taille négative. En bref, si nous obtenons un mécanisme d'expression constante des instructions, nous aurons d'autres options ouvertes.
Daniel Santos
Quoi qu'il en soit, nous ne pouvons pas utiliser ces macro pour une variable. droite? error: bit-field ‘<anonymous>’ width not an integer constantIl n'autorise que les constantes. Alors, à quoi ça sert?
Karthik Raj Palanichamy du
1
@Karthik Recherchez les sources du noyau Linux pour voir pourquoi il est utilisé.
Daniel Santos
@supercat Je ne vois pas du tout comment votre commentaire est lié. Pouvez-vous s'il vous plaît le réviser, mieux expliquer ce que vous voulez dire ou le supprimer?
Daniel Santos
36

Il crée un 0champ binaire de taille si la condition est fausse, mais un champ binaire size -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 rien de tel qu'un -1champ de bits de taille n'est créé, bien sûr).

Matt Phillips
la source
3
En fait, il renvoie un size_tavec une valeur 0 au cas où la condition est vraie.
David Heffernan