Perché usare istruzioni do-while e if-else apparentemente prive di significato nelle macro?

StackOverflow https://stackoverflow.com/questions/154136

  •  03-07-2019
  •  | 
  •  

Domanda

In molte macro C / C ++ vedo il codice della macro racchiuso in quello che sembra un ciclo insignificante mentre . 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 sta facendo il mentre . Perché non scrivere questo senza di esso?

#define FOO(X) f(X); g(X)
È stato utile?

Soluzione

Il do ... while e if ... else sono lì per farlo in modo che un punto e virgola dopo la macro significa sempre la stessa cosa. Diciamo te aveva qualcosa come la tua seconda macro.

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

Ora se dovessi usare BAR (X); in un'istruzione if ... else , dove i corpi dell'istruzione if non erano racchiusi tra parentesi graffe , avresti una brutta sorpresa.

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

Il codice sopra si espanderebbe in

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

che è sintatticamente errato, poiché l'altro non è più associato all'if. Non aiuta a avvolgere le cose tra parentesi graffe all'interno della macro, perché un punto e virgola dopo le parentesi graffe è sintatticamente errato.

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

Esistono due modi per risolvere il problema. Il primo è usare una virgola per mettere in sequenza le istruzioni all'interno della macro senza privarla della sua capacità di agire come un'espressione.

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

La versione precedente della barra BAR espande il codice sopra in quello che segue, che è sintatticamente corretto.

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

Questo non funziona se invece di f (X) hai un corpo di codice più complicato che deve andare nel suo blocco, ad esempio per dichiarare le variabili locali. Nel caso più generale, la soluzione è utilizzare qualcosa come do ... while per fare in modo che la macro sia una singola istruzione che richiede un punto e virgola senza confusione.

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

Non devi usare do ... mentre , potresti anche cucinare qualcosa con if ... else , anche se quando if ... else si espande all'interno di un se ... else porta ad un " dangling else " ;, che potrebbe rendere ancora più difficile trovare un problema esistente di dangling, come nel codice seguente.

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

Il punto è usare il punto e virgola in contesti in cui un punto e virgola penzolante è errato. Naturalmente, a questo punto si potrebbe (e probabilmente dovrebbe) essere sostenuto che sarebbe meglio dichiarare BAR come una funzione reale, non una macro.

In breve, il do ... while è lì per aggirare le carenze del preprocessore C. Quando quelle guide di stile C ti dicono di licenziare il preprocessore C, questo è il tipo di cosa di cui sono preoccupati.

Altri suggerimenti

Le macro sono pezzi di testo copiati / incollati che il pre-processore inserirà nel codice originale; l'autore della macro spera che la sostituzione produca un codice valido.

Esistono tre buoni suggerimenti "quot". per riuscirci:

Aiuta la macro a comportarsi come un vero codice

Il codice normale è di solito terminato da un punto e virgola. Se l'utente visualizza il codice che non necessita di uno ...

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 il vero vero buon motivo è che a un certo punto, l'autore della macro dovrà forse sostituire la macro con una funzione autentica (forse incorporata). Quindi la macro dovrebbe davvero comportarsi come una.

Quindi dovremmo avere una macro che necessita di punti 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, questo 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) ;

Produci un codice valido 2

Se la macro è simile a:

#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, ancora una volta, la soluzione utilizza 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 semi-colon + effetti scope?

Esiste un linguaggio 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 così nel codice che ne ha bisogno.

Il bonus?

Il compilatore C ++ ottimizzerà il ciclo do / while, poiché il fatto che la sua post-condizione sia 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 come

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

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

   // Etc.
}

@ jfm3 - Hai una bella risposta alla domanda. Potresti anche aggiungere che il linguaggio macro previene anche il comportamento involontario forse più pericoloso (perché non c'è errore) con semplici istruzioni "if":

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

if (test) FOO( baz);

si espande in:

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

che è sintatticamente corretto quindi non c'è nessun errore del compilatore, ma ha la conseguenza probabilmente non intenzionale che g () sarà sempre chiamato.

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 if ... else .

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

FOO(1)
printf("abc");

Anche se abbiamo escluso il punto e virgola (per errore), il codice si espanderà in

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

e verrà compilato silenziosamente (anche se alcuni compilatori potrebbero emettere un avviso per codice non raggiungibile). Ma l'istruzione printf non verrà mai eseguita.

Il costrutto

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

Mentre ci si aspetta che i compilatori ottimizzino il do {...} mentre (false); scorre, esiste un'altra soluzione che non richiederebbe quel costrutto. La soluzione è utilizzare l'operatore virgola:

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

o ancora più esoticamente:

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

Anche se funzionerà bene con istruzioni separate, non funzionerà con i casi in cui le variabili sono costruite e utilizzate come parte del #define :

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

Con questo sarebbe costretto a usare il costrutto do / while.

La libreria del preprocessore P99 di Jens Gustedt (sì, il fatto che esista una cosa del genere mi ha colpito attenzione anche!) migliora il if (1) {...} else in 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, a differenza del do {...} while (0) , il break e continue funzionano ancora all'interno del blocco dato, ma ((void) 0) crea un errore di sintassi se il punto e virgola viene omesso dopo la chiamata macro, che altrimenti salta il blocco successivo. (In realtà non esiste qui un problema "ciondolante altro", poiché else si lega al più vicino se , che è quello nella macro.)

Se sei interessato a cose che possono essere fatte più o meno in sicurezza con il preprocessore C, dai un'occhiata a quella libreria.

Per alcuni motivi non posso commentare la prima risposta ...

Alcuni di voi hanno mostrato macro con variabili locali, ma nessuno ha detto che non si può semplicemente usare un nome in una macro! Un giorno morderà l'utente! Perché? Perché gli argomenti di input vengono sostituiti nel modello di macro. E nei tuoi esempi macro hai usato il nome variabile probabilmente più comunemente usato i .

Ad esempio quando la seguente macro

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

è utilizzato nella seguente funzione

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

la macro non utilizzerà la variabile prevista i, che è dichiarata all'inizio di some_func, ma la variabile locale, che è dichiarata nel ciclo do ... while della macro.

Quindi, non usare mai nomi di variabili comuni in una macro!

Spiegazione

do {} while (0) e if (1) {} else devono assicurarsi che la macro sia espansa a 1 sola istruzione. In caso contrario:

if (something)
  FOO(X); 

si espanderebbe in:

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

E g (X) verrebbero eseguiti al di fuori dell'istruzione di controllo if . Questo è evitato quando si utilizza do {} while (0) e if (1) {} else .


Migliore alternativa

Con una GNU espressione (non parte dello standard C), hai un modo migliore di do {} while (0) e if (1) {} else per risolvere questo, semplicemente usando ({}) :

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

E questa sintassi è compatibile con i valori di ritorno (notare che do {} mentre (0) non lo è), come in:

return FOO("X");

Non credo sia stato menzionato, quindi considera questo

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

verrebbe tradotto in

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

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

Ho trovato questo trucco molto utile in situazioni in cui è necessario elaborare in sequenza un determinato valore. Ad ogni livello di elaborazione, se si verificano errori o condizioni non valide, è possibile evitare ulteriori elaborazioni e interrompere anticipatamente. per es.

#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);
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top