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

Em muitas macros C / C ++, estou vendo o código da macro envolto no que parece ser um loop 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 o que do while está fazendo. Por que não apenas escrever isso sem isso?

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

O do ... while e if ... else estão lá para fazer com que um ponto-e-vírgula após a macro sempre signifique a mesma coisa. Digamos que você tenha algo parecido com sua segunda macro.

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

Agora, se você fosse usar BAR(X); em uma declaração if ... else , em que os corpos da instrução if não eram agrupados em chaves, você teria uma surpresa ruim.

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

O código acima se expandiria

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

que está sintaticamente incorreto, já que o else não está mais associado ao if. Não ajuda a embrulhar as coisas entre chaves dentro da macro, porque um ponto-e-vírgula depois das chaves está sintaticamente incorreto.

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

Existem duas maneiras de corrigir o problema. A primeira é usar uma vírgula para sequenciar 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 BAR expande o código acima para o que segue, o que é sintaticamente correto.

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

Isso não funciona se em vez de f(X) você tiver um corpo de código mais complicado que precisa ir em seu próprio bloco, por exemplo, para declarar variables ​​locais. No caso mais geral, a solução é usar algo como do ... while para fazer com que a macro seja uma única instrução que use 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 tem que usar do ... while , você poderia cozinhar algo com if ... else bem, embora quando if ... else expande dentro de um if ... else leva a um ” pendente ” “, o que poderia tornar ainda mais difícil encontrar um problema pendente, 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 pendente é errôneo. Naturalmente, poderia (e provavelmente deveria) ser argumentado neste ponto que seria melhor declarar BAR como uma function real, não uma macro.

Em resumo, o do ... while está lá para contornar as deficiências do pré-processador C. Quando esses guias de estilo C lhe dizem para dispensar o pré-processador C, esse é o tipo de coisa que eles estão preocupados.

Macros são pedaços de texto copiados / colados que o pré-processador colocará no código original; o autor da macro espera que a substituição produza um código válido.

Existem três boas “dicas” para ter sucesso nisso:

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

Normalmente, o código normal é terminado por um ponto e vírgula. O usuário deve ver 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 a verdadeira boa razão é que, em algum momento, talvez o autor da macro precise replace a macro por uma function genuína (talvez embutida). Portanto, a macro deve realmente se comportar como uma.

Então, devemos ter uma macro precisando de ponto e vírgula.

Produza 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) ; 

Esta macro pode ser expandida como:

 #define MY_MACRO(x) f(x) ; g(x) if(bIsOk) f(42) ; g(42) ; // was MY_MACRO(42) ; 

A function g será executada independentemente do valor de bIsOk .

Isso significa que devemos ter que adicionar um escopo à macro:

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

Produza um código válido 2

Se a macro é algo como:

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

Poderíamos ter outro problema no seguinte código:

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

Porque se expandiria como:

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

Este código não irá compilar, é 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 semi-colônias + efeitos de escopo?

Existe 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 assim para o código que precisa de um.

O bônus?

O compilador C ++ otimizará o loop do / while, já que o fato de sua condição posterior ser falsa é conhecido em tempo de compilation. 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. } 

irá 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. } 

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

 #define FOO(x) f(x); g(x) if (test) FOO( baz); 

expande para:

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

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

As respostas acima explicam o significado desses construtos, mas há uma diferença significativa entre os dois que não foi mencionada. De fato, há uma razão para preferir o do ... while ao construtor if ... else .

O problema da construção if ... else é que ela não força você 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 se expandirá para

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

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

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

Enquanto é esperado que os compiladores otimizem o do { ... } while(false); loops, existe outra solução que não exigiria essa construção. A solução é usar o operador 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, ele não funcionará com casos em que variables ​​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.

A biblioteca de pré-processadores P99 de Jens Gustedt (sim, o fato de que tal coisa também tenha me surpreendido!) Melhora o if(1) { ... } else constrói de uma forma 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 razão para isto é que, ao contrário do { ... } while(0) construct, break e continue ainda funcionam dentro do bloco dado, mas o ((void)0) cria um erro de syntax se o ponto-e-vírgula é omitido após o chamada de macro, que de outra forma ignoraria o próximo bloco. (Na verdade, não há um problema “pendente”, já que o else liga-se ao if mais próximo, que é o que está na macro.)

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

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

Alguns de vocês mostraram macros com variables ​​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 input são substituídos em seu modelo de macro. E em seus exemplos de macro, você usa o nome variavelmente provavelmente mais usado i .

Por exemplo, quando a macro a seguir

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

é usado na seguinte function

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

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

Assim, nunca use nomes de variables ​​comuns em uma macro!

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

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

seria traduzido em

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

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

Eu encontrei este truque muito útil é em situações onde você tem que processar seqüencialmente um valor particular. Em cada nível de processamento, se algum erro ou condição inválida ocorrer, você poderá evitar o processamento adicional e sair mais cedo. por exemplo

 #define CALL_AND_RETURN(x) if ( x() == false) break; do { CALL_AND_RETURN(process_first); CALL_AND_RETURN(process_second); CALL_AND_RETURN(process_third); //(simply add other calls here) } while (0); 

Explicação

do {} while (0) e if (1) {} else são para assegurar que a macro seja expandida para apenas 1 instrução. De outra forma:

 if (something) FOO(X); 

expandir-se-ia para:

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

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


Melhor alternativa

Com uma expressão de declaração GNU (não faz parte do padrão C), você tem uma maneira melhor do do {} while (0) e if (1) {} else para resolver isso, simplesmente usando ({}) :

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

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

 return FOO("X");