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


788

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

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

2
Per l'esempio con l'altro, aggiungerei un'espressione di voidtipo alla fine ... like ((void) 0) .
Phil1970,

1
Ricorda che il do whilecostrutto non è compatibile con le dichiarazioni di ritorno, quindi il if (1) { ... } else ((void)0)costrutto ha usi più compatibili nello Standard C. E in GNU C, preferirai il costrutto descritto nella mia risposta.
Cœur

Risposte:


829

Il do ... whilee if ... elsesono lì per farlo in modo che un punto e virgola dopo la macro significhi sempre la stessa cosa. Supponiamo che tu abbia qualcosa come la tua seconda macro.

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

Ora, se dovessi usare BAR(X);in if ... elseun'istruzione, in cui i corpi dell'istruzione if non fossero racchiusi tra parentesi graffe, otterrai 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 BARespande 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)avere un corpus di codice più complicato che deve andare nel proprio blocco, ad esempio per dichiarare le variabili locali. Nel caso più generale, la soluzione è utilizzare qualcosa di simile do ... whileper 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 ... while, potresti anche cucinare qualcosa if ... else, anche se quando si if ... elseespande all'interno di un if ... elseporta a un " altro penzolante ", che potrebbe rendere ancora più difficile trovare un problema esistente penzolante, 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 BARcome una funzione reale, non una macro.

In sintesi, do ... whilec'è 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.


18
Non è questo un argomento forte per usare sempre le parentesi graffe nelle dichiarazioni if, while e for? Se si punta sempre a fare ciò (come richiesto per MISRA-C, ad esempio), il problema sopra descritto scompare.
Steve Melnikoff,

17
L'esempio di virgola dovrebbe essere #define BAR(X) (f(X), g(X))altrimenti la precedenza dell'operatore potrebbe incasinare la semantica.
Stewart,

20
@DawidFerenczy: sebbene sia io che te, da quattro anni e mezzo, facciamo un buon punto, dobbiamo vivere nel mondo reale. A meno che non possiamo garantire che tutte le ifistruzioni, ecc., Nel nostro codice utilizzino le parentesi graffe, avvolgere le macro in questo modo è un modo semplice per evitare problemi.
Steve Melnikoff,

8
Nota: il if(1) {...} else void(0)modulo è più sicuro di quello do {...} while(0)per le macro i cui parametri sono codice incluso nell'espansione della macro, perché non altera il comportamento dell'interruzione o continua le parole chiave. Ad esempio: for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }provoca un comportamento imprevisto quando MYMACROè definito come #define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)perché l'interruzione influisce sul ciclo while della macro anziché sul ciclo for nel sito di chiamata della macro.
Chris Kline,

5
@ace void(0)era un errore di battitura, intendevo dire (void)0. E credo che questo non risolve il "penzoloni altro" problema: avviso non c'è nessun punto e virgola dopo il (void)0. Un altro penzolante in quel caso (es. if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }) Innesca un errore di compilazione. Ecco un esempio dal vivo che lo dimostra
Chris Kline,

153

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.

Ci sono tre buoni "consigli" per riuscirci:

Aiuta la macro a comportarsi come un vero codice

Il codice normale di solito termina con un punto e virgola. Se l'utente visualizza il codice che 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 il vero vero buon motivo è che a un certo punto, l'autore della macro dovrà forse sostituire la macro con una funzione autentica (forse in linea). Quindi la macro dovrebbe comportarsi davvero come una.

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

Produrre 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 gfunzione 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) ;

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

6
Si noti che la modifica delle macro in funzione incorporata altera alcune macro predefinite standard, ad esempio il codice seguente mostra una modifica in FUNZIONE e LINEA : #include <stdio.h> #define Fmacro () printf ("% s% d \ n", FUNCTION , LINE ) inline void Finline () {printf ("% s% d \ n", FUNCTION , LINE ); } int main () {Fmacro (); Finline (); ritorna 0; } (i termini in grassetto devono essere racchiusi tra doppi caratteri di sottolineatura - cattivo formattatore!)
Gnubie,

6
Ci sono una serie di problemi minori ma non del tutto irrilevanti con questa risposta. Ad esempio: void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }non è l'espansione corretta; l' xespansione dovrebbe essere 32. Una questione più complessa è qual è l'espansione di MY_MACRO(i+7). E un altro è l'espansione di MY_MACRO(0x07 << 6). C'è molto di buono, ma ci sono alcuni non annotati e non incrociati.
Jonathan Leffler il

@Gnubie: suppongo che tu sia ancora qui e non l'hai ancora capito: puoi scappare da asterischi e caratteri di sottolineatura nei commenti con barre rovesciate, quindi se digiti \_\_LINE\_\_viene visualizzato come __LINE__. IMHO, è meglio usare solo la formattazione del codice per il codice; ad esempio, __LINE__(che non richiede alcuna gestione speciale). PS Non so se questo fosse vero nel 2012; da allora hanno apportato alcuni miglioramenti al motore.
Scott,

1
Apprezzando il fatto che il mio commento sia in ritardo di sei anni, ma la maggior parte dei compilatori C in realtà non inlineincorpora le funzioni (come consentito dallo standard)
Andrew

53

@ jfm3 - Hai una bella risposta alla domanda. Potresti anche aggiungere che l'idioma 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 () verrà sempre chiamato.


22
"probabilmente non voluto"? Avrei detto "sicuramente non intenzionale", altrimenti il ​​programmatore deve essere tirato fuori e sparato (al contrario di essere stato castigato con una frusta).
Lawrence Dol,

4
Oppure potrebbero ricevere un aumento, se lavorano per un'agenzia di tre lettere e inseriscono di nascosto quel codice in un programma open source molto usato ... :-)
R .. GitHub STOP HELPING ICE

2
E questo commento mi ricorda solo la goto fail line nel recente bug di verifica del certificato SSL trovato nei sistemi operativi Apple
Gerard Sexton

23

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 ... whileal if ... elsecostrutto.

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

FOO(1)
printf("abc");

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

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 la printfdichiarazione non verrà mai eseguita.

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


2
@RichardHansen: Ancora non buono, perché dall'esame della macro invocazione non si sa se si espande in un'istruzione o in un'espressione. Se qualcuno assume il dopo, può scrivere FOO(1),x++;che ci darà di nuovo un falso positivo. Basta usare do ... whilee basta.
Yakov Galka,

1
Documentare la macro per evitare l'incomprensione dovrebbe essere sufficiente. Sono d'accordo che do ... while (0)sia preferibile, ma ha un aspetto negativo: A breako continuecontrollerà il do ... while (0)ciclo, non il ciclo contenente l'invocazione macro. Quindi il iftrucco ha ancora valore.
Richard Hansen,

2
Non vedo dove potresti mettere uno breako uno continueche potrebbe essere visto come uno do {...} while(0)pseudo-ciclo all'interno delle tue macro . Anche nel parametro macro farebbe un errore di sintassi.
Patrick Schlüter,

5
Un altro motivo da usare do { ... } while(0)invece di if whatevercostruire, è la sua natura idiomatica. Il do {...} while(0)costrutto è diffuso, ben noto e utilizzato molto da molti programmatori. La sua logica e la documentazione sono prontamente note. Non così per il ifcostrutto. Ci vuole quindi meno sforzo per grok quando si fa la revisione del codice.
Patrick Schlüter,

1
@tristopia: ho visto persone scrivere macro che accettano blocchi di codice come argomenti (che non consiglio necessariamente). Ad esempio: #define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0. Potrebbe essere usato come CHECK(system("foo"), break;);, dove break;si intende fare riferimento al ciclo che racchiude l' CHECK()invocazione.
Richard Hansen,

16

Mentre ci si aspetta che i compilatori ottimizzino i do { ... } while(false);loop, 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)))

Mentre questo funzionerà bene con istruzioni separate, non funzionerà con i casi in cui le variabili sono costruite e utilizzate come parte di #define:

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

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


grazie, poiché l'operatore virgola non garantisce l'ordine di esecuzione, questo annidamento è un modo per imporlo.
Marius,

15
@Marius: False; l'operatore virgola è un punto di sequenza e quindi fa garanzia ordine di esecuzione. Sospetto che tu l'abbia confuso con la virgola negli elenchi degli argomenti delle funzioni.
R .. GitHub smette di aiutare ICE il

2
Il secondo suggerimento esotico ha reso la mia giornata.
Spidey,

Volevo solo aggiungere che i compilatori sono costretti a preservare il comportamento osservabile del programma, quindi l'ottimizzazione del do / while away non è un grosso problema (supponendo che le ottimizzazioni del compilatore siano corrette).
Marco A.

@MarcoA. benché tu abbia ragione, in passato ho scoperto che l'ottimizzazione del compilatore, preservando esattamente la funzione del codice, ma cambiando le linee che sembrerebbero non fare nulla nel singolare contesto, romperà gli algoritmi multithread. Caso in questione Peterson's Algorithm.
Marius,

11

La libreria del preprocessore P99 di Jens Gustedt (sì, anche il fatto che esista una cosa del genere mi ha fatto impazzire!) Migliora il if(1) { ... } elsecostrutto 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

Il razionale per questo è che, a differenza del do { ... } while(0)costrutto, breake continuefunzionano ancora all'interno del blocco specificato, ma la ((void)0)crea un errore di sintassi se la virgola viene omesso dopo la chiamata macro, altrimenti saltare il blocco successivo. (Non c'è in realtà un problema "penzolante altro" qui, dal momento che si elselega al più vicino if, 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.


Sebbene molto intelligente, questo fa sì che si sia bombardati con avvertimenti del compilatore su potenziali penzoloni.
Segmentato il

1
Di solito usi le macro per creare un ambiente contenuto, cioè non usi mai una break(o continue) all'interno di una macro per controllare un ciclo iniziato / finito all'esterno, che è solo un cattivo stile e nasconde potenziali punti di uscita.
mirabilos,

C'è anche una libreria preprocessore in Boost. Cosa c'è di strabiliante a riguardo?
Rene,

Il rischio else ((void)0)è che qualcuno stia scrivendo YOUR_MACRO(), f();e sarà sintatticamente valido, ma non chiamerà mai f(). Con do whileè un errore di sintassi.
melpomene,

@melpomene e allora else do; while (0)?
Carl Lei,

8

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)

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

Pertanto, non utilizzare mai nomi di variabili comuni in una macro!


Il solito schema è quello di aggiungere caratteri di sottolineatura nei nomi delle variabili nelle macro, ad esempio int __i;.
Blaisorblade,

8
@Blaisorblade: In realtà è C errato e illegale; i caratteri di sottolineatura principali sono riservati per l'implementazione. La ragione per cui hai visto questo "solito schema" è dalla lettura delle intestazioni di sistema ("l'implementazione") che devono limitarsi a questo spazio dei nomi riservato. Per applicazioni / librerie, è necessario scegliere i propri nomi oscuri e improbabili da scontrarsi senza caratteri di sottolineatura, ad esempio mylib_internal___io simili.
R .. GitHub smette di aiutare ICE il

2
@R .. Hai ragione: in realtà l'ho letto in una '' applicazione '', il kernel Linux, ma è comunque un'eccezione poiché non utilizza alcuna libreria standard (tecnicamente, un'implementazione C '' indipendente '' invece di uno "ospitato").
Blaisorblade,

3
@R .. questo non è del tutto corretto: i trattini di sottolineatura iniziali seguiti da un trattino basso o da un trattino basso sono riservati per l'implementazione in tutti i contesti. I caratteri di sottolineatura principali seguiti da qualcos'altro non sono riservati nell'ambito locale.
Leushenko,

@Leushenko: Sì, ma la distinzione è sufficientemente sottile che trovo meglio dire alle persone solo di non usare tali nomi. Le persone che comprendono la sottigliezza presumibilmente sanno già che sto sorvolando i dettagli. :-)
R .. GitHub FERMA AIUTANDO ICE

7

Spiegazione

do {} while (0)e if (1) {} elseassicurarsi 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 ifcontrol. Questo è evitato quando si usa do {} while (0)e if (1) {} else.


Alternativa migliore

Con un'espressione dell'istruzione GNU (non parte dello standard C), hai un modo migliore di do {} while (0)e if (1) {} elseper risolverlo, semplicemente usando ({}):

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

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

return FOO("X");

3
l'uso del blocco dei blocchi {} nella macro sarebbe sufficiente per raggruppare il codice macro in modo che tutto venga eseguito per lo stesso percorso if-condition. il do-while around viene utilizzato per imporre un punto e virgola nei punti in cui viene utilizzata la macro. pertanto la macro viene forzata comportando più funzioni allo stesso modo. questo include il requisito per il punto e virgola finale quando usato.
Alexander Stohr,

2

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.


15
Questo non ha nulla a che fare con il costrutto do ... while (0).
Trento,

2
Vero. Ma pertinente al tema delle macro rispetto alle funzioni e al modo di scrivere una macro che si comporta come una funzione ...
John Nilsson,

2
Analogamente a quanto sopra, questa non è una risposta ma un commento. A do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
proposito
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.