O que é ":-!!" no código C?

1665

Eu encontrei esse código de macro estranho em /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); }))

O que :-!!faz?

chmurli
fonte
2
- Unário menos <br />! Lógica NÃO <br /> inversa não não do dado número inteiro e por isso a variável pode ser 0 ou 1.
CyrillC
69
A culpa do git nos diz que essa forma específica de afirmação estática foi introduzida por Jan Beulich em 8c87df4 . Obviamente, ele tinha boas razões para fazê-lo (veja a mensagem de confirmação).
Niklas B.
55
@Lundin: assert () NÃO causa um erro em tempo de compilação. Esse é o objetivo da construção acima.
Chris Pacejo
4
@GreweKokkor Não seja ingênuo, o Linux é grande demais para uma pessoa lidar com tudo. Linus tem seus tenentes e os deles, que impulsionam mudanças e melhorias de baixo para cima. Linus apenas decide se quer ou não uma participação, mas confia em colegas de trabalho até certo ponto. Se você quiser saber mais sobre como o sistema distribuído funciona em ambiente de código aberto, consulte o vídeo do youtube: youtube.com/watch?v=4XpnKHJAok8 (é uma conversa muito interessante).
Tomas Pruzina
3
@cpcloud, sizeof"avalia" o tipo, mas não o valor. É o tipo que é inválido neste caso.
Winston Ewert

Respostas:

1692

Esta é, com efeito, uma maneira de verificar se a expressão e pode ser avaliada como 0 e, se não, falhar na construção .

A macro é um pouco errada; deveria ser algo mais parecido BUILD_BUG_OR_ZERO, ao invés de ...ON_ZERO. (Houve discussões ocasionais sobre se esse é um nome confuso .)

Você deve ler a expressão assim:

sizeof(struct { int: -!!(e); }))
  1. (e): Computar expressão e.

  2. !!(e): Negar logicamente duas vezes: 0se e == 0; caso contrário 1.

  3. -!!(e): Negue numericamente a expressão da etapa 2: 0se foi 0; caso contrário -1.

  4. struct{int: -!!(0);} --> struct{int: 0;}: Se fosse zero, declaramos uma estrutura com um campo de bits inteiro anônimo que possui largura zero. Está tudo bem e prosseguimos normalmente.

  5. struct{int: -!!(1);} --> struct{int: -1;}: Por outro lado, se não for zero, será um número negativo. Declarar qualquer campo de bits com largura negativa é um erro de compilação.

Portanto, terminaremos com um campo de bits que possui largura 0 em uma estrutura, o que é bom ou um campo de bits com largura negativa, que é um erro de compilação. Em seguida, pegamos sizeofesse campo e obtemos um size_tcom a largura apropriada (que será zero no caso em que eé zero).


Algumas pessoas perguntaram: Por que não usar apenas um assert?

A resposta de keithmo aqui tem uma boa resposta:

Essas macros implementam um teste em tempo de compilação, enquanto assert () é um teste em tempo de execução.

Exatamente certo. Você não deseja detectar problemas no seu kernel em tempo de execução que poderiam ter sido detectados anteriormente! É uma parte crítica do sistema operacional. Em qualquer extensão, os problemas podem ser detectados em tempo de compilação, tanto melhor.

John Feminella
fonte
5
@weston Muitos lugares diferentes. Veja por si mesmo!
John Feminella
166
variantes recentes dos padrões C ++ ou C têm algo semelhante static_assertpara fins relacionados.
Basile Starynkevitch
54
@Lundin - #error exigiria o uso de 3 linhas de código # if / # error / # endif e funcionaria apenas para avaliações acessíveis ao pré-processador. Esse hack funciona para qualquer avaliação acessível ao compilador.
10132 Ed Staub
236
O kernel do Linux não usa C ++, pelo menos não enquanto o Linus ainda está vivo.
Mark Ransom
6
@ Dolda2000: " Expressões booleanas em C são definidas para sempre avaliar zero ou um " - Não exatamente. Os operadores que produzem resultados "logicamente booleanas" ( !, <, >, <=, >=, ==, !=, &&, ||) sempre produzem 0 ou 1. Outras expressões podem proporcionar resultados que podem ser utilizados como um condições, mas são meramente zero ou diferente de zero; por exemplo, isdigit(c)onde cé um dígito, pode gerar qualquer valor diferente de zero (que é tratado como verdadeiro em uma condição).
amigos estão dizendo sobre keith thompson
256

O :é um campo de bits. Quanto a !!, isso é dupla negação lógica e, portanto, retorna 0para falso ou 1verdadeiro. E o -é um sinal de menos, ou seja, negação aritmética.

É apenas um truque para fazer com que o compilador vomite em entradas inválidas.

Considere BUILD_BUG_ON_ZERO. Quando -!!(e)avalia para um valor negativo, isso produz um erro de compilação. Caso contrário, será -!!(e)avaliado como 0, e um campo de bits com largura 0 terá tamanho 0 e, portanto, a macro será avaliada como a size_tcom valor 0.

O nome é fraco, na minha opinião, porque a construção falha de fato quando a entrada não é zero.

BUILD_BUG_ON_NULLé muito semelhante, mas gera um ponteiro em vez de um int.

David Heffernan
fonte
14
está sizeof(struct { int:0; })estritamente em conformidade?
ouah
7
Por que o resultado seria em geral 0? A structcom apenas um campo de bits vazio, é verdade, mas não acho que a estrutura com tamanho 0 seja permitida. Por exemplo, se você criar uma matriz desse tipo, os elementos individuais da matriz ainda deverão ter endereços diferentes, não?
Jens Gustedt
2
eles na verdade não se importam, pois usam extensões GNU, desabilitam a regra estrita de alias e não consideram o excesso de número inteiro como UB. Mas eu queria saber se isso é estritamente em conformidade C.
ouah
3
@ouah sobre anônimas bitfields comprimento zero, consulte aqui: stackoverflow.com/questions/4297095/...
David Heffernan
9
@DavidHeffernan, na verdade, C permite um campo de bits sem 0largura de largura, mas não se não houver outro membro nomeado na estrutura. (C99, 6.7.2.1p2) "If the struct-declaration-list contains no named members, the behavior is undefined."Por exemplo, sizeof (struct {int a:1; int:0;})é estritamente conforme, mas sizeof(struct { int:0; })não é (comportamento indefinido).
OuJá
168

Algumas pessoas parecem estar confundindo essas macros assert().

Essas macros implementam um teste em tempo de compilação, enquanto assert()é um teste em tempo de execução.

keithmo
fonte
52

Bem, estou surpreso que as alternativas a essa sintaxe não tenham sido mencionadas. Outro mecanismo comum (mas mais antigo) é chamar uma função que não está definida e confiar no otimizador para compilar a chamada de função se sua afirmação estiver correta.

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

Embora esse mecanismo funcione (desde que as otimizações estejam ativadas), ele tem a desvantagem de não relatar um erro até você vincular, quando não consegue encontrar a definição para a função you_did_something_bad (). É por isso que os desenvolvedores do kernel começam a usar truques, como as larguras de campo de bits de tamanho negativo e as matrizes de tamanho negativo (o último dos quais parou de quebrar as compilações no GCC 4.4).

Simpatizando com a necessidade de asserções em tempo de compilação, o GCC 4.3 introduziu o erroratributo function que permite estender esse conceito mais antigo, mas gera um erro em tempo de compilação com uma mensagem de sua escolha - não mais uma matriz de tamanho negativo "enigmática" " mensagens de erro!

#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 fato, a partir do Linux 3.9, agora temos uma macro chamada compiletime_assertque usa esse recurso e a maioria das macros bug.hforam atualizadas de acordo. Ainda assim, essa macro não pode ser usada como inicializador. No entanto, usando expressões de declaração (outra extensão C do GCC), você pode!

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

Essa macro avaliará seu parâmetro exatamente uma vez (caso tenha efeitos colaterais) e criará um erro em tempo de compilação que diz "Eu disse para você não me dar cinco!" se a expressão for avaliada como cinco ou não for uma constante em tempo de compilação.

Então, por que não estamos usando isso em vez de campos de bits de tamanho negativo? Infelizmente, atualmente existem muitas restrições ao uso de expressões de instrução, incluindo o uso como inicializadores constantes (para constantes enum, largura de campo de bits etc.), mesmo que a expressão de instrução seja completamente constante, ela própria (ou seja, pode ser totalmente avaliada em tempo de compilação e passa no __builtin_constant_p()teste). Além disso, eles não podem ser usados ​​fora de um corpo funcional.

Felizmente, o GCC alterará essas deficiências em breve e permitirá que expressões constantes de instruções sejam usadas como inicializadores constantes. O desafio aqui é a especificação da linguagem que define o que é uma expressão constante legal. O C ++ 11 adicionou a palavra-chave constexpr apenas para esse tipo ou coisa, mas nenhuma contrapartida existe no C11. Embora o C11 tenha obtido afirmações estáticas, o que resolverá parte desse problema, ele não resolverá todas essas deficiências. Então, espero que o gcc possa disponibilizar uma funcionalidade constexpr como uma extensão via -std = gnuc99 & -std = gnuc11 ou algo parecido e permitir seu uso nas expressões de declaração et. al.

Daniel Santos
fonte
6
Todas as suas soluções NÃO são alternativas. O comentário acima do macro é bastante clara " so the expression can be used e.g. in a structure initializer (or where-ever else comma expressions aren't permitted)." A macro retorna uma expressão do tiposize_t
Wiz
3
@ Wiz Sim, eu estou ciente disso. Talvez isso tenha sido um pouco detalhado e talvez eu precise reexaminar minhas palavras, mas meu objetivo era explorar os vários mecanismos de afirmações estáticas e mostrar por que ainda estamos usando campos de bits de tamanho negativo. Em resumo, se obtivermos um mecanismo para a expressão constante de instruções, teremos outras opções em aberto.
Daniel Santos
De qualquer forma, não podemos usar essas macro para uma variável. direita? error: bit-field ‘<anonymous>’ width not an integer constantEstá permitindo apenas constantes. Então, qual é a utilidade?
Karthik Raj Palanichamy
1
@Karthik Pesquise as fontes do kernel do Linux para ver por que ele é usado.
Daniel Santos
@ supercat Não vejo como o seu comentário está relacionado. Você pode revisá-lo, explicar melhor o que você quer dizer ou removê-lo?
Daniel Santos
36

Ele está criando um campo de 0bits de tamanho se a condição for falsa, mas um campo de bit de tamanho -1( -!!1) se a condição for verdadeira / diferente de zero. No primeiro caso, não há erro e a estrutura é inicializada com um membro int. No último caso, há um erro de compilação (e -1, naturalmente , não existe um campo de bits de tamanho ).

Matt Phillips
fonte
3
Na verdade, ele está retornando um size_tcom valor 0, caso a condição seja verdadeira.
David Heffernan