Por que usar instruções do-while e if-else aparentemente sem sentido nas macros?

788

Em muitas macros C / C ++, estou vendo o código da macro envolvido no que parece ser um do whileloop sem sentido . Aqui estão alguns exemplos.

#define FOO(X) do { f(X); g(X); } while (0)
#define FOO(X) if (1) { f(X); g(X); } else

Não consigo ver o que do whileestá fazendo. Por que não escrever isso sem ele?

#define FOO(X) f(X); g(X)
jfm3
fonte
2
Para o exemplo com o else, eu adicionaria uma expressão do voidtipo no final ... como ((void) 0) .
precisa saber é o seguinte
1
Lembre-se de que a do whileconstrução não é compatível com as declarações de retorno; portanto, a if (1) { ... } else ((void)0)construção tem usos mais compatíveis no Padrão C. E no GNU C, você prefere a construção descrita na minha resposta.
Cœur

Respostas:

829

O do ... whilee if ... elseexiste para que um ponto-e-vírgula após a macro sempre signifique a mesma coisa. Digamos que você tenha algo parecido com a sua segunda macro.

#define BAR(X) f(x); g(x)

Agora, se você usar BAR(X);em uma if ... elsedeclaração, onde os corpos da declaração if não estiverem entre colchetes, você terá uma surpresa ruim.

if (corge)
  BAR(corge);
else
  gralt();

O código acima seria expandido para

if (corge)
  f(corge); g(corge);
else
  gralt();

que é sintaticamente incorreto, pois o resto não está mais associado ao if. Não ajuda a agrupar as coisas entre chaves na macro, porque um ponto-e-vírgula após as chaves é sintaticamente incorreto.

if (corge)
  {f(corge); g(corge);};
else
  gralt();

Existem duas maneiras de corrigir o problema. O primeiro é usar uma vírgula para sequenciar as instruções dentro da macro sem roubar sua capacidade de agir como uma expressão.

#define BAR(X) f(X), g(X)

A versão acima da barra BARexpande o código acima para o que segue, que é sintaticamente correto.

if (corge)
  f(corge), g(corge);
else
  gralt();

Isso não funciona se, em vez de f(X)você possuir um corpo de código mais complicado que precisar ser inserido em seu próprio bloco, digamos, por exemplo, para declarar variáveis ​​locais. No caso mais geral, a solução é usar algo como do ... whilefazer com que a macro seja uma única declaração que leva um ponto-e-vírgula sem confusão.

#define BAR(X) do { \
  int i = f(X); \
  if (i > 4) g(i); \
} while (0)

Você não precisa usá- do ... whilelo, também pode criar algo if ... else, embora, quando se if ... elseexpanda dentro de um if ... else, leve a um " dangling else ", o que poderia tornar um problema de dangling else existente ainda mais difícil de encontrar, como no código a seguir .

if (corge)
  if (1) { f(corge); g(corge); } else;
else
  gralt();

O ponto é usar o ponto e vírgula em contextos em que um ponto e vírgula oscilante é incorreto. Certamente, poderia (e provavelmente deveria) argumentar-se neste ponto que seria melhor declarar BARcomo uma função real, não como uma macro.

Em resumo, do ... whileexiste para solucionar as deficiências do pré-processador C. Quando esses guias de estilo C dizem para você demitir o pré-processador C, esse é o tipo de coisa com a qual eles estão preocupados.

jfm3
fonte
18
Este não é um argumento forte para sempre usar chaves em declarações if, while e for? Se você sempre faz isso (como é necessário para o MISRA-C, por exemplo), o problema descrito acima desaparece.
Steve Melnikoff
17
O exemplo de vírgula deve ser de #define BAR(X) (f(X), g(X))outra maneira a precedência do operador pode atrapalhar a semântica.
Stewart
20
@DawidFerenczy: embora você e eu, de quatro anos e meio atrás, façam um bom argumento, temos que viver no mundo real. A menos que possamos garantir que todas as ifinstruções, etc, em nosso código usem chaves, agrupar macros como essa é uma maneira simples de evitar problemas.
Steve Melnikoff
8
Nota: o if(1) {...} else void(0)formulário é mais seguro que o do {...} while(0)das macros cujos parâmetros são o código incluído na expansão da macro, porque não altera o comportamento da quebra nem continua as palavras-chave. Por exemplo: for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }causa um comportamento inesperado quando MYMACROé definido como #define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)porque a interrupção afeta o loop while da macro em vez do loop for no site de chamada da macro.
Chris Kline
5
@ace void(0)foi um erro de digitação, eu quis dizer (void)0. E acredito que isso não resolver o "pendendo mais" problema: aviso não há nenhuma vírgula após o (void)0. Uma outra oscilação nesse caso (por exemplo if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }) dispara um erro de compilação. Aqui está um exemplo ao vivo demonstrando
Chris Kline
153

Macros são partes de texto copiadas / coladas que o pré-processador colocará no código original; o autor da macro espera que a substituição produza código válido.

Existem três boas "dicas" para ter sucesso nisso:

Ajude a macro a se comportar como código genuíno

O código normal geralmente termina com um ponto e vírgula. O usuário deve visualizar o código sem precisar de um ...

doSomething(1) ;
DO_SOMETHING_ELSE(2)  // <== Hey? What's this?
doSomethingElseAgain(3) ;

Isso significa que o usuário espera que o compilador produza um erro se o ponto e vírgula estiver ausente.

Mas o verdadeiro motivo realmente bom é que, em algum momento, o autor da macro talvez precise substituir a macro por uma função genuína (talvez embutida). Portanto, a macro deve realmente se comportar como uma.

Portanto, devemos ter uma macro precisando de ponto e vírgula.

Produzir um código válido

Como mostrado na resposta do jfm3, às vezes a macro contém mais de uma instrução. E se a macro for usada dentro de uma instrução if, isso será problemático:

if(bIsOk)
   MY_MACRO(42) ;

Essa macro pode ser expandida como:

#define MY_MACRO(x) f(x) ; g(x)

if(bIsOk)
   f(42) ; g(42) ; // was MY_MACRO(42) ;

A gfunção será executada independentemente do valor de bIsOk.

Isso significa que precisamos adicionar um escopo à macro:

#define MY_MACRO(x) { f(x) ; g(x) ; }

if(bIsOk)
   { f(42) ; g(42) ; } ; // was MY_MACRO(42) ;

Produzir um código válido 2

Se a macro é algo como:

#define MY_MACRO(x) int i = x + 1 ; f(i) ;

Podemos ter outro problema no seguinte código:

void doSomething()
{
    int i = 25 ;
    MY_MACRO(32) ;
}

Porque expandiria como:

void doSomething()
{
    int i = 25 ;
    int i = 32 + 1 ; f(i) ; ; // was MY_MACRO(32) ;
}

Esse código não será compilado, é claro. Então, novamente, a solução está usando um escopo:

#define MY_MACRO(x) { int i = x + 1 ; f(i) ; }

void doSomething()
{
    int i = 25 ;
    { int i = 32 + 1 ; f(i) ; } ; // was MY_MACRO(32) ;
}

O código se comporta corretamente novamente.

Combinando ponto-e-vírgula + efeitos de escopo?

Há um idioma C / C ++ que produz este efeito: O loop do / while:

do
{
    // code
}
while(false) ;

O do / while pode criar um escopo, encapsulando o código da macro, e precisa de um ponto e vírgula no final, expandindo para o código que precisa de um.

O bônus?

O compilador C ++ otimizará o loop do / while, pois o fato de sua pós-condição ser falsa é conhecido no momento da compilação. Isso significa que uma macro como:

#define MY_MACRO(x)                                  \
do                                                   \
{                                                    \
    const int i = x + 1 ;                            \
    f(i) ; g(i) ;                                    \
}                                                    \
while(false)

void doSomething(bool bIsOk)
{
   int i = 25 ;

   if(bIsOk)
      MY_MACRO(42) ;

   // Etc.
}

expandirá corretamente como

void doSomething(bool bIsOk)
{
   int i = 25 ;

   if(bIsOk)
      do
      {
         const int i = 42 + 1 ; // was MY_MACRO(42) ;
         f(i) ; g(i) ;
      }
      while(false) ;

   // Etc.
}

e é então compilado e otimizado como

void doSomething(bool bIsOk)
{
   int i = 25 ;

   if(bIsOk)
   {
      f(43) ; g(43) ;
   }

   // Etc.
}
paercebal
fonte
6
Observe que alterar macros para a função embutida altera algumas macros padrão predefinidas, por exemplo, o código a seguir mostra uma alteração em FUNCTION e LINE : #include <stdio.h> #define Fmacro () printf ("% s% d \ n", FUNCTION , LINE ) inline void Finline () {printf ("% s% d \ n", FUNÇÃO , LINHA ); } int main () {Fmacro (); Finline (); retornar 0; } (Termos em negrito deve ser fechado por sublinhados duplos - bad formatador!)
Gnubie
6
Há uma série de questões menores, mas não completamente inconseqüentes, com esta resposta. Por exemplo: void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }não é a expansão correta; o xda expansão deve ser 32. Uma questão mais complexa é do que é a expansão MY_MACRO(i+7). E outra é a expansão de MY_MACRO(0x07 << 6). Há muito que é bom, mas existem alguns i e não cruzados.
precisa saber é o seguinte
@Gnubie: Caso você ainda esteja aqui e ainda não tenha entendido isso: você pode escapar de asteriscos e sublinhados em comentários com barras invertidas; portanto, se você digitar, \_\_LINE\_\_ele será renderizado como __LINE__. IMHO, é melhor usar apenas a formatação de código; por exemplo, __LINE__(que não requer nenhum tratamento especial). PS: Não sei se isso era verdade em 2012; eles fizeram algumas melhorias no mecanismo desde então.
Scott
1
Apreciando que o meu comentário é de seis anos de atraso, mas a maioria dos compiladores C realmente não em linha inlinefunções (conforme permitido pela norma)
Andrew
53

@ jfm3 - Você tem uma boa resposta para a pergunta. Você também pode adicionar que o idioma da macro também evita o comportamento não intencional possivelmente mais perigoso (porque não há erro) com instruções simples 'se':

#define FOO(x)  f(x); g(x)

if (test) FOO( baz);

expande para:

if (test) f(baz); g(baz);

que é sintaticamente correto, portanto não há erro do compilador, mas tem a conseqüência provavelmente não intencional de que g () sempre será chamado.

Michael Burr
fonte
22
"provavelmente não intencional"? Eu teria dito "certamente não intencionalmente", ou então o programador precisa ser retirado e fuzilado (ao contrário de castigado com um chicote).
Lawrence Dol
4
Ou eles podem receber um aumento, se estiverem trabalhando para uma agência de três letras e inserindo clandestinamente esse código em um programa de código aberto amplamente usado ... :-)
R .. GitHub PARE DE AJUDAR O GELO
2
E este comentário só me faz lembrar do Goto falhar linha na recente bug de verificação SSL cert encontrados em Apple sistemas operacionais
Gerard Sexton
23

As respostas acima explicam o significado dessas construções, mas há uma diferença significativa entre as duas que não foi mencionada. De fato, há uma razão para preferir do ... whileo if ... elseconstruto.

O problema da if ... elseconstrução é que ela não o força a colocar o ponto e vírgula. Como neste código:

FOO(1)
printf("abc");

Embora tenhamos deixado de fora o ponto-e-vírgula (por engano), o código será expandido para

if (1) { f(X); g(X); } else
printf("abc");

e será compilado silenciosamente (embora alguns compiladores possam emitir um aviso para código inacessível). Mas a printfdeclaração nunca será executada.

do ... whileA construção não tem esse problema, pois o único token válido após o while(0)é um ponto e vírgula.

Yakov Galka
fonte
2
@RichardHansen: Ainda não é tão bom, porque, pela análise da invocação de macro, você não sabe se ela se expande para uma declaração ou expressão. Se alguém assumir o que é mais tarde, ela pode escrever o FOO(1),x++;que novamente nos dará um falso positivo. Basta usar do ... whilee é isso.
Yakov Galka
1
Documentar a macro para evitar mal-entendidos deve ser suficiente. Concordo que do ... while (0)é preferível, mas tem uma desvantagem: A breakou continuecontrolará o do ... while (0)loop, não o loop que contém a chamada de macro. Portanto, o iftruque ainda tem valor.
Richard Hansen
2
Não vejo onde você poderia colocar um breakou um continueque seria visto como dentro do seu do {...} while(0)pseudo-loop de macros . Mesmo no parâmetro macro, cometeria um erro de sintaxe.
Patrick Schlüter
5
Outro motivo para usar em do { ... } while(0)vez de if whateverconstruir é a natureza idiomática. A do {...} while(0)construção é generalizada, bem conhecida e muito utilizada por muitos programadores. Sua lógica e documentação são facilmente conhecidas. Não é assim para a ifconstrução. Portanto, é preciso menos esforço para fazer o grok ao fazer a revisão do código.
Patrick Schlüter
1
@tristopia: Eu já vi pessoas escreverem macros que usam blocos de código como argumentos (o que eu não recomendo necessariamente). Por exemplo: #define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0. Pode ser usado como CHECK(system("foo"), break;);, onde o break;se destina a se referir ao loop que encerra a CHECK()invocação.
Richard Hansen
16

Embora seja esperado que os compiladores otimizem os do { ... } while(false);loops, existe outra solução que não exigiria essa construção. A solução é usar o operador de vírgula:

#define FOO(X) (f(X),g(X))

ou ainda mais exoticamente:

#define FOO(X) g((f(X),(X)))

Embora isso funcione bem com instruções separadas, não funcionará com casos em que variáveis ​​são construídas e usadas como parte do #define:

#define FOO(X) (int s=5,f((X)+s),g((X)+s))

Com isso, seria forçado a usar a construção do / while.

Marius
fonte
obrigado, como o operador de vírgula não garante a ordem de execução, esse aninhamento é uma maneira de impor isso.
Marius
15
@ Marius: Falso; o operador de vírgula é um ponto de sequência e, portanto , garante a ordem de execução. Eu suspeito que você tenha confundido com a vírgula nas listas de argumentos das funções.
R .. GitHub Pare de ajudar o
2
A segunda sugestão exótica fez o meu dia.
Spidey
Só queria acrescentar que os compiladores são forçados a preservar o comportamento observável do programa, portanto, otimizar o fazer / enquanto ausente não é grande coisa (supondo que as otimizações do compilador estejam corretas).
Marco A.
@MarcoA. enquanto você estiver correto, descobri no passado que a otimização do compilador, preservando exatamente a função do código, mas alterando as linhas que parecem não fazer nada no contexto singular, quebrará os algoritmos multithread. Caso em questão Peterson's Algorithm.
Marius
11

A biblioteca de pré-processadores P99 de Jens Gustedt (sim, o fato de que isso existe também me impressionou !) Melhora a if(1) { ... } elseconstrução de uma maneira pequena, mas significativa, definindo o seguinte:

#define P99_NOP ((void)0)
#define P99_PREFER(...) if (1) { __VA_ARGS__ } else
#define P99_BLOCK(...) P99_PREFER(__VA_ARGS__) P99_NOP

A lógica para isso é que, diferentemente da do { ... } while(0)construção, breake continueainda funciona dentro do bloco especificado, ele ((void)0)cria um erro de sintaxe se o ponto-e-vírgula for omitido após a chamada de macro, que, caso contrário, ignoraria o próximo bloco. (Na verdade, não há um problema "dangling else" aqui, pois ele elsese liga ao mais próximo if, que é o da macro.)

Se você estiver interessado nos tipos de coisas que podem ser feitas com mais ou menos segurança com o pré-processador C, consulte essa biblioteca.

Isaac Schwabacher
fonte
Embora seja muito inteligente, isso faz com que alguém seja bombardeado com avisos do compilador sobre possíveis outros danos.
Segmentado
1
Você geralmente usa macros para criar um ambiente contido, ou seja, nunca usa um break(ou continue) dentro de uma macro para controlar um loop que começou / terminou fora, isso é apenas um estilo ruim e oculta pontos de saída em potencial.
mirabilos
Há também uma biblioteca de pré-processadores no Boost. O que é alucinante sobre isso?
Rene
O risco else ((void)0)é que alguém esteja escrevendo YOUR_MACRO(), f();e seja sintaticamente válido, mas nunca ligue f(). Com do whileisso é um erro de sintaxe.
Melpomene
@melpomene então que tal else do; while (0)?
Carl
8

Por algumas razões, não posso comentar a primeira resposta ...

Alguns de vocês mostraram macros com variáveis ​​locais, mas ninguém mencionou que você não pode simplesmente usar qualquer nome em uma macro! Vai morder o usuário algum dia! Por quê? Porque os argumentos de entrada são substituídos no seu modelo de macro. E em seus exemplos de macro, você usa o nome variado provavelmente mais usado, i .

Por exemplo, quando a seguinte macro

#define FOO(X) do { int i; for (i = 0; i < (X); ++i) do_something(i); } while (0)

é usado na seguinte função

void some_func(void) {
    int i;
    for (i = 0; i < 10; ++i)
        FOO(i);
}

a macro não usará a variável pretendida i, declarada no início de some_func, mas a variável local, declarada no loop do ... while da macro.

Portanto, nunca use nomes de variáveis ​​comuns em uma macro!

Mike Meyer
fonte
O padrão usual é adicionar sublinhados nos nomes das variáveis ​​nas macros - por exemplo int __i;.
Blaisorblade
8
@ Blaisorblade: Na verdade isso é incorreto e ilegal C; sublinhados principais são reservados para uso pela implementação. O motivo pelo qual você viu esse "padrão usual" é a leitura dos cabeçalhos do sistema ("a implementação"), que devem se restringir a esse espaço de nome reservado. Para aplicativos / bibliotecas, você deve escolher seus próprios nomes obscuros e improváveis ​​de colidir sem sublinhados, por exemplo, mylib_internal___iou similares.
R .. GitHub Pare de ajudar o gelo
2
@R .. Você está certo - eu realmente li isso em um '' aplicativo '', o kernel do Linux, mas é uma exceção de qualquer maneira, pois ele não usa biblioteca padrão (tecnicamente, uma implementação C '' independente '' de um '' hospedado '').
Bluesorblade
3
@R .. isso não está totalmente correto: os sublinhados iniciais seguidos por um capital ou segundo sublinhado são reservados para a implementação em todos os contextos. Os sublinhados principais seguidos por outra coisa não são reservados no escopo local.
Leushenko
@Leushenko: Sim, mas a distinção é suficientemente sutil que eu acho melhor dizer às pessoas para não usarem esses nomes. As pessoas que entendem a sutileza presumivelmente já sabem que estou encobrindo os detalhes. :-)
R .. GitHub Pare de ajudar o gelo
7

Explicação

do {} while (0)e if (1) {} elsecertifique-se de que a macro seja expandida para apenas 1 instrução. De outra forma:

if (something)
  FOO(X); 

expandiria para:

if (something)
  f(X); g(X); 

E g(X)seria executado fora da ifinstrução de controle. Isso é evitado ao usar do {} while (0)e if (1) {} else.


Melhor alternativa

Com uma expressão de instrução GNU (não faz parte do padrão C), você tem uma maneira melhor do que do {} while (0)e if (1) {} elseresolvê-lo, simplesmente usando ({}):

#define FOO(X) ({f(X); g(X);})

E essa sintaxe é compatível com valores de retorno (observe que do {} while (0)não é), como em:

return FOO("X");
Cœur
fonte
3
o uso de fixação de bloco {} na macro seria suficiente para agrupar o código da macro, para que tudo seja executado para o mesmo caminho de condição se. o do-while around é usado para impor um ponto-e-vírgula nos locais em que a macro é usada. assim, a macro é aplicada comportando-se de maneira mais funcional. isso inclui o requisito para o ponto e vírgula à direita quando usado.
Alexander Stohr
2

Eu não acho que foi mencionado, então considere isso

while(i<100)
  FOO(i++);

seria traduzido para

while(i<100)
  do { f(i++); g(i++); } while (0)

observe como i++é avaliado duas vezes pela macro. Isso pode levar a alguns erros interessantes.

John Nilsson
fonte
15
Isso não tem nada a ver com a construção do ... while (0).
Trent
2
Verdade. Mas relevent ao tópico de macros vs funções e como escrever uma macro que se comporta como uma função ...
John Nilsson
2
Da mesma forma que a anterior, esta não é uma resposta, mas um comentário. No tópico: é por isso que você usa as coisas apenas uma vez:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
mirabilos