Que es ":-!!" en el código C?

1665

Me topé con este extraño código macro en /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); }))

¿Qué :-!!hacer?

chmurli
fuente
2
- Unario menos <br />! Lógico NO <br /> inverso no no del entero e dado, por lo que la variable puede ser 0 o 1.
CyrillC
69
git blame nos dice que Jan Beulich introdujo esta forma particular de afirmación estática en 8c87df4 . Obviamente tenía buenas razones para hacerlo (ver el mensaje de confirmación).
Niklas B.
55
@Lundin: afirmar () NO causa un error en tiempo de compilación. Ese es el objetivo de la construcción anterior.
Chris Pacejo
44
@GreweKokkor No seas ingenuo, Linux es demasiado grande para que una persona lo maneje todo. Linus tiene sus lugartenientes y ellos tienen los suyos que impulsan los cambios y las mejoras de abajo hacia arriba. Linus solo decide si quiere una característica o no, pero confía en sus compañeros de trabajo hasta cierto punto. Si quieres saber más sobre cómo funciona el sistema distribuido en un entorno de código abierto, mira el video de youtube: youtube.com/watch?v=4XpnKHJAok8 (es una charla muy interesante).
Tomás Pruzina
3
@cpcloud, sizeof"evalúa" el tipo, pero no el valor. Es el tipo que no es válido en este caso.
Winston Ewert

Respuestas:

1692

Esta es, en efecto, una forma de verificar si la expresión e puede evaluarse como 0, y si no, fallar la compilación .

La macro está mal nombrada; debería ser algo más parecido BUILD_BUG_OR_ZERO, en lugar de ...ON_ZERO. (Ha habido discusiones ocasionales sobre si este es un nombre confuso ).

Deberías leer la expresión así:

sizeof(struct { int: -!!(e); }))
  1. (e): Calcular expresión e.

  2. !!(e): Negar lógicamente dos veces: 0si e == 0; de otro modo 1.

  3. -!!(e): Numéricamente niega la expresión del paso 2: 0si lo fue 0; de otro modo -1.

  4. struct{int: -!!(0);} --> struct{int: 0;}: Si fue cero, entonces declaramos una estructura con un campo de bits entero anónimo que tiene un ancho cero. Todo está bien y procedemos normalmente.

  5. struct{int: -!!(1);} --> struct{int: -1;}: Por otro lado, si no es cero, entonces será un número negativo. Declarar cualquier campo de bits con ancho negativo es un error de compilación.

Así que terminaremos con un campo de bits que tiene un ancho 0 en una estructura, que está bien, o un campo de bits con ancho negativo, que es un error de compilación. Luego tomamos sizeofese campo, por lo que obtenemos un size_tcon el ancho apropiado (que será cero en el caso donde esea ​​cero).


Algunas personas han preguntado: ¿por qué no solo usar un assert?

La respuesta de Keithmo aquí tiene una buena respuesta:

Estas macros implementan una prueba de tiempo de compilación, mientras afirma () es una prueba de tiempo de ejecución.

Exactamente correcto. ¡No querrá detectar problemas en su núcleo en tiempo de ejecución que podrían haberse detectado antes! Es una pieza crítica del sistema operativo. En cualquier medida, los problemas se pueden detectar en tiempo de compilación, tanto mejor.

John Feminella
fuente
55
@weston Muchos lugares diferentes. ¡Ver por ti mismo!
John Feminella
166
Las variantes recientes de los estándares C ++ o C tienen algo similar static_assertpara fines relacionados.
Basile Starynkevitch
54
@Lundin - #error requeriría el uso de 3 líneas de código # if / # error / # endif, y solo funcionaría para evaluaciones accesibles para el preprocesador. Este truco funciona para cualquier evaluación accesible para el compilador.
Ed Staub
236
El kernel de Linux no usa C ++, al menos no mientras Linus todavía está vivo.
Mark Ransom
66
@ Dolda2000: "Las expresiones booleanas en C se definen para evaluar siempre a cero o uno " - No exactamente. Los operadores que producen resultados "lógicamente booleanos" ( !, <, >, <=, >=, ==, !=, &&, ||) siempre producen 0 o 1. Otras expresiones pueden producir resultados que pueden ser utilizados como una condiciones, pero son meramente cero o distinto de cero; por ejemplo, isdigit(c)donde ces un dígito, puede producir cualquier valor distinto de cero (que luego se trata como verdadero en una condición).
Keith Thompson el
256

El :es un campo de bits. En cuanto a !!, esa es la doble negación lógica y, por lo tanto, devuelve 0falso o 1verdadero. Y el -es un signo menos, es decir, negación aritmética.

Todo es solo un truco para que el compilador vomite entradas no válidas.

Considere BUILD_BUG_ON_ZERO. Cuando se -!!(e)evalúa a un valor negativo, eso produce un error de compilación. De lo contrario, se -!!(e)evalúa a 0, y un campo de bits de ancho 0 tiene un tamaño de 0. Y, por lo tanto, la macro se evalúa a a size_tcon valor 0.

El nombre es débil en mi opinión porque la construcción de hecho falla cuando la entrada no es cero.

BUILD_BUG_ON_NULLes muy similar, pero produce un puntero en lugar de un int.

David Heffernan
fuente
14
es sizeof(struct { int:0; })estrictamente conforme?
ouah
77
¿Por qué sería el resultado en general 0? A structcon solo un campo de bits vacío, cierto, pero no creo que se permitan estructuras con tamaño 0. Por ejemplo, si crea una matriz de ese tipo, los elementos individuales de la matriz aún deben tener direcciones diferentes, ¿no?
Jens Gustedt
2
en realidad no les importa, ya que usan extensiones GNU, deshabilitan la estricta regla de alias y no consideran los desbordamientos de enteros como UB. Pero me preguntaba si esto es estrictamente conforme C.
ouah
3
@ouah con respecto a los campos de bits de longitud cero sin nombre, ver aquí: stackoverflow.com/questions/4297095/…
David Heffernan
99
@DavidHeffernan en realidad C permite un campo de 0ancho de bit sin nombre , pero no si no hay otro miembro nombrado en la estructura. (C99, 6.7.2.1p2) "If the struct-declaration-list contains no named members, the behavior is undefined."Entonces, por ejemplo, sizeof (struct {int a:1; int:0;})es estrictamente conforme pero sizeof(struct { int:0; })no lo es (comportamiento indefinido).
ouah
168

Algunas personas parecen estar confundiendo estas macros con assert().

Estas macros implementan una prueba de tiempo de compilación, mientras que assert()es una prueba de tiempo de ejecución.

keithmo
fuente
52

Bueno, estoy bastante sorprendido de que no se hayan mencionado las alternativas a esta sintaxis. Otro mecanismo común (pero más antiguo) es llamar a una función que no está definida y confiar en el optimizador para compilar la llamada a la función si su afirmación es correcta.

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

Si bien este mecanismo funciona (siempre y cuando las optimizaciones estén habilitadas) tiene el inconveniente de no informar un error hasta que se enlace, momento en el cual no puede encontrar la definición de la función you_did_something_bad (). Es por eso que los desarrolladores de kernel comienzan a usar trucos como los anchos de campo de bits de tamaño negativo y las matrices de tamaño negativo (la última de las cuales dejó de romper las compilaciones en GCC 4.4).

En simpatía por la necesidad de afirmaciones en tiempo de compilación, GCC 4.3 introdujo el erroratributo de función que le permite ampliar este concepto anterior, pero generar un error en tiempo de compilación con un mensaje de su elección: no más críptico "matriz de tamaño negativo" " ¡error de mensajes!

#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)

De hecho, a partir de Linux 3.9, ahora tenemos una macro llamada compiletime_assertque utiliza esta función y la mayoría de las macros bug.hse han actualizado en consecuencia. Aún así, esta macro no se puede usar como inicializador. Sin embargo, si usa expresiones de enunciado (otra extensión C de GCC), ¡puede hacerlo!

#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;                                                    \
    })

Esta macro evaluará su parámetro exactamente una vez (en caso de que tenga efectos secundarios) y creará un error en tiempo de compilación que dice "¡Te dije que no me dieras cinco!" si la expresión se evalúa a cinco o no es una constante en tiempo de compilación.

Entonces, ¿por qué no estamos usando esto en lugar de campos de bits de tamaño negativo? Por desgracia, actualmente existen muchas restricciones en el uso de expresiones de enunciados, incluido su uso como inicializadores constantes (para constantes enum, ancho de campo de bits, etc.) incluso si la expresión de enunciados es completamente constante en sí misma (es decir, puede evaluarse completamente en tiempo de compilación y de lo contrario pasa la __builtin_constant_p()prueba). Además, no se pueden usar fuera del cuerpo de una función.

Con suerte, GCC enmendará estas deficiencias pronto y permitirá que se utilicen expresiones de enunciados constantes como inicializadores constantes. El desafío aquí es la especificación del lenguaje que define lo que es una expresión legal constante. C ++ 11 agregó la palabra clave constexpr solo para este tipo o cosa, pero no existe una contraparte en C11. Si bien C11 obtuvo aserciones estáticas, que resolverán parte de este problema, no resolverá todas estas deficiencias. Así que espero que gcc pueda hacer que una funcionalidad constexpr esté disponible como una extensión a través de -std = gnuc99 & -std = gnuc11 o algo así y permitir su uso en expresiones de declaración et. Alabama.

Daniel Santos
fuente
66
Todas sus soluciones NO son alternativas. El comentario sobre la macro es bastante claro " so the expression can be used e.g. in a structure initializer (or where-ever else comma expressions aren't permitted)." La macro devuelve una expresión de tiposize_t
Wiz
3
@Wiz Sí, soy consciente de esto. Quizás esto fue un poco detallado y tal vez necesito volver a visitar mi redacción, pero mi punto era explorar los diversos mecanismos para las afirmaciones estáticas y mostrar por qué todavía estamos usando campos de bits de tamaño negativo. En resumen, si obtenemos un mecanismo para la expresión de declaraciones constantes, tendremos otras opciones abiertas.
Daniel Santos
De todos modos, no podemos usar estas macro para una variable. ¿Correcto? error: bit-field ‘<anonymous>’ width not an integer constantSolo permite constantes. Entonces, ¿de qué sirve?
Karthik Raj Palanichamy
1
@Karthik Busque en las fuentes del kernel de Linux para ver por qué se usa.
Daniel Santos
@supercat No veo cómo está relacionado tu comentario en absoluto. ¿Puedes revisarlo, explicar mejor lo que quieres decir o eliminarlo?
Daniel Santos
36

Está creando un 0campo de bits de tamaño si la condición es falsa, pero un campo de bits de tamaño -1( -!!1) si la condición es verdadera / distinta de cero. En el primer caso, no hay error y la estructura se inicializa con un miembro int. En el último caso, hay un error de compilación (y -1, por supuesto, no se crea un campo de bits de tamaño ).

Matt Phillips
fuente
3
En realidad, devuelve un size_tvalor 0 en caso de que la condición sea verdadera.
David Heffernan