[c++] Perché usare affermazioni do-while e if-else apparentemente prive di significato nelle macro?



Answers

Le macro sono copie / parti di testo incollate che il pre-processore inserirà nel codice originale; l'autore della macro spera che la sostituzione produrrà un codice valido.

Ci sono tre buoni "consigli" per avere successo in questo:

Aiuta la macro a comportarsi come un vero codice

Normalmente il codice normale termina con un punto e virgola. Se l'utente visualizza il codice non ne ha bisogno ...

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

Ciò significa che l'utente si aspetta che il compilatore produca un errore se il punto e virgola è assente.

Ma la vera vera buona ragione è che a un certo punto, l'autore della macro avrà forse bisogno di sostituire la macro con una funzione autentica (forse in linea). Quindi la macro dovrebbe davvero comportarsi come tale.

Quindi dovremmo avere una macro che necessita di punto e virgola.

Produce un codice valido

Come mostrato nella risposta di jfm3, a volte la macro contiene più di un'istruzione. E se la macro viene utilizzata all'interno di un'istruzione if, ciò sarà problematico:

if(bIsOk)
   MY_MACRO(42) ;

Questa macro potrebbe essere espansa come:

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

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

La funzione g verrà eseguita indipendentemente dal valore di bIsOk .

Ciò significa che dobbiamo aggiungere un ambito alla macro:

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

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

Produce un codice valido 2

Se la macro è qualcosa di simile:

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

Potremmo avere un altro problema nel seguente codice:

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

Perché si espanderebbe come:

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

Questo codice non verrà compilato, ovviamente. Quindi, di nuovo, la soluzione sta usando un ambito:

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

Il codice si comporta di nuovo correttamente.

Combinazione di effetti del punto e virgola + ambito?

C'è un idioma C / C ++ che produce questo effetto: Il ciclo do / while:

do
{
    // code
}
while(false) ;

Il do / while può creare un ambito, incapsulando così il codice della macro, e alla fine ha bisogno di un punto e virgola, espandendosi quindi nel codice che ne richiede uno.

Il bonus?

Il compilatore C ++ ottimizzerà il ciclo do / while, poiché il fatto che la sua post-condizione è falsa è noto al momento della compilazione. Ciò significa che una macro come:

#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.
}

si espanderà correttamente come

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 viene quindi compilato e ottimizzato via come

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

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

   // Etc.
}
Question

In molti macro C / C ++ vedo il codice della macro avvolto in quello che sembra un loop senza senso. Ecco alcuni esempi

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

Non riesco a vedere cosa do while fa. Perché non scrivere semplicemente questo senza di esso?

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



La libreria del preprocessore P99 di Jens Gustedt (sì, il fatto che esista anche una cosa del genere!) Migliora il if(1) { ... } else costrutto in un modo piccolo ma significativo definendo quanto segue:

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

La logica di ciò è che, diversamente dal costrutto do { ... } while(0) , break e continue funzionano ancora all'interno del blocco dato, ma il ((void)0) crea un errore di sintassi se il punto e virgola viene omesso dopo il chiamata macro, che altrimenti salta il blocco successivo. (In realtà non c'è un problema "dangling else", poiché il else lega al più vicino if , che è quello nella macro.)

Se sei interessato a quel genere di cose che possono essere fatte più o meno in sicurezza con il preprocessore C, controlla quella libreria.




Ho trovato questo trucco molto utile in situazioni in cui è necessario elaborare un valore particolare in modo sequenziale. Ad ogni livello di elaborazione, se si verifica un errore o una condizione non valida, è possibile evitare ulteriori elaborazioni e scoppiare in anticipo. per esempio

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



Non penso che sia stato menzionato, quindi considera questo

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

sarebbe tradotto in

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

notare come i++ viene valutato due volte dalla macro. Questo può portare ad alcuni errori interessanti.




Le risposte sopra spiegano il significato di questi costrutti, ma c'è una differenza significativa tra i due che non è stata menzionata. In effetti, c'è un motivo per preferire il do ... while al costrutto if ... else .

Il problema del costrutto if ... else è che non ti obbliga a mettere il punto e virgola. Come in questo codice:

FOO(1)
printf("abc");

Anche se abbiamo lasciato fuori il punto e virgola (per errore), il codice si espanderà a

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

e verrà compilato in modo invisibile all'utente (sebbene alcuni compilatori possano inviare un avviso per codice non raggiungibile). Ma l'istruzione printf non verrà mai eseguita.

do ... while costrutto non ha questo problema, poiché l'unico token valido dopo un while(0) è un punto e virgola.




Related