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

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) 

Il do ... while e if ... else ci sono per farlo in modo che un punto e virgola dopo la tua macro significhi sempre la stessa cosa. Diciamo che hai qualcosa come la tua seconda macro.

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

Ora se dovessi usare BAR(X); in una dichiarazione if ... else , in cui i corpi dell’istruzione if non erano racchiusi tra parentesi graffe, si ottiene una brutta sorpresa.

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

Il codice di cui sopra si espanderebbe in

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

che è sintatticamente errato, poiché il resto non è più associato al if. Non aiuta a racchiudere le cose tra parentesi graffe all’interno della macro, poiché un punto e virgola dopo le parentesi graffe è sintatticamente errato.

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

Ci sono due modi per risolvere il problema. Il primo consiste nell’usare una virgola in istruzioni di sequenza all’interno della macro senza privarla della sua capacità di agire come un’espressione.

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

La versione precedente di BAR espande il codice sopra in quanto 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 variabili locali. Nel caso più generale la soluzione è usare qualcosa come do ... while per far sì che la macro sia una singola istruzione che prende 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 ... while , potresti anche inventare qualcosa con if ... else anche se quando if ... else espande all’interno di un if ... else porta a un ” dangling else” “, che potrebbe rendere ancora più difficile trovare un problema ancora presente, come nel seguente codice.

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

Il punto è utilizzare il punto e virgola in contesti in cui un punto e virgola pendente è errato. Naturalmente, a questo punto potrebbe (e probabilmente dovrebbe) essere discusso sul fatto che sarebbe meglio dichiarare BAR come una funzione reale, non una macro.

In sintesi, 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 genere di cose di cui sono preoccupati.

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

@ jfm3 – Hai una bella risposta alla domanda. Si potrebbe anche voler aggiungere che l’idioma macro previene anche il ansible comportamento non intenzionale più pericoloso (perché non c’è errore) con semplici istruzioni “if”:

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

si espande a:

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

Mentre è previsto che i compilatori ottimizzino il do { ... } while(false); cicli, c’è un’altra soluzione che non richiederebbe quel costrutto. La soluzione è utilizzare l’operatore virgola:

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

o anche più estaticamente:

 #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 si sarebbe costretti a usare il costrutto do / while.

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 syntax 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.

Per alcune ragioni 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! Morderà l’utente un giorno! Perché? Perché gli argomenti di input sono sostituiti nel modello macro. E nei tuoi esempi macro hai usato il nome variabile probabilmente usato più comunemente io .

Ad esempio quando la seguente macro

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

è usato nella seguente funzione

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

la macro non utilizzerà la variabile desiderata 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!

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.

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 qualche errore o condizione non valida, è ansible 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); 

Spiegazione

do {} while (0) e if (1) {} else per assicurarsi che la macro sia espansa a solo 1 istruzione. Altrimenti:

 if (something) FOO(X); 

si espanderebbe a:

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

E g(X) verrebbe eseguito al di fuori dell’istruzione if control. Questo è evitato quando si usa do {} while (0) e if (1) {} else .


Migliore alternativa

Con un’espressione di istruzione GNU (non una 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 syntax è compatibile con i valori di ritorno (nota che do {} while (0) non lo è), come in:

 return FOO("X");