it-swarm.com.de

Warum scheinbar sinnlose do-while- und if-else-Anweisungen in Makros verwenden?

In vielen C/C++ - Makros sehe ich den Code des Makros in einer scheinbar sinnlosen do while-Schleife. Hier sind Beispiele.

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

Ich kann nicht sehen, was der do while tut. Warum schreiben Sie das nicht einfach ohne?

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

Der do ... while und if ... else dienen dazu, dass ein Semikolon ____ nach Ihrem Makro immer dasselbe bedeutet. Angenommen, Sie hatten etwas wie Ihr zweites Makro.

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

Wenn Sie BAR(X); in einer if ... else-Anweisung verwenden würden, bei der die Körper der if-Anweisung nicht in geschweiften Klammern eingeschlossen waren, würden Sie eine böse Überraschung erleben.

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

Der obige Code würde sich in erweitern

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

was syntaktisch falsch ist, da das else nicht mehr mit dem if verbunden ist. Es hilft nicht, Dinge in geschweifte Klammern innerhalb des Makros zu packen, da ein Semikolon nach den Klammern syntaktisch falsch ist.

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

Es gibt zwei Möglichkeiten, das Problem zu beheben. Die erste besteht darin, ein Komma zu verwenden, um Anweisungen innerhalb des Makros zu sequenzieren, ohne es seiner Fähigkeit zu berauben, sich wie ein Ausdruck zu verhalten.

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

Die obige Version von bar BAR erweitert den obigen Code in den folgenden Code, was syntaktisch korrekt ist.

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

Dies funktioniert nicht, wenn Sie anstelle von f(X) einen komplizierteren Code-Code haben, der in einen eigenen Block gehen muss, z. B. um lokale Variablen zu deklarieren. Im Allgemeinen ist die Lösung die Verwendung von etwas wie do ... while, um zu bewirken, dass das Makro eine einzelne Anweisung ist, die ohne Verwirrung ein Semikolon enthält.

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

Sie müssen nicht do ... while verwenden, Sie könnten auch etwas mit if ... else zubereiten, obwohl if ... else sich innerhalb eines if ... else ausdehnt, was zu einem " dangling else " führt, was ein bestehendes schwebendes else-Problem noch schwieriger machen könnte zu finden, wie im folgenden Code.

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

Es geht darum, das Semikolon in Kontexten zu verwenden, in denen ein hängendes Semikolon fehlerhaft ist. Natürlich könnte (und sollte dies wahrscheinlich) an dieser Stelle argumentiert werden, dass es besser wäre, BAR als tatsächliche Funktion und nicht als Makro zu deklarieren.

Zusammenfassend ist der do ... while dazu da, die Mängel des C-Präprozessors zu umgehen. Wenn diese C-Style-Guides Ihnen sagen, dass Sie den C-Präprozessor entlassen müssen, machen sie sich darüber Sorgen.

752
jfm3

Makros sind kopierte/eingefügte Textstücke, die der Vorprozessor in den Originalcode einfügt. Der Autor des Makros hofft, dass der Ersatz gültigen Code ergibt.

Es gibt drei gute "Tipps", um das zu erreichen:

Helfen Sie dem Makro, sich wie authentischer Code zu verhalten

Normaler Code wird normalerweise durch ein Semikolon abgeschlossen. Wenn der Benutzer den Code nicht benötigt, ...

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

Dies bedeutet, dass der Benutzer erwartet, dass der Compiler einen Fehler ausgibt, wenn das Semikolon fehlt.

Aber der wirklich gute Grund ist, dass der Autor des Makros das Makro zu einer bestimmten Zeit durch eine echte Funktion (möglicherweise inline) ersetzen muss. Also sollte sich das Makro wirklich wie eines verhalten.

Wir sollten also ein Makro haben, das ein Semikolon benötigt.

Produzieren Sie einen gültigen Code

Wie in der Antwort von jfm3 gezeigt, enthält das Makro manchmal mehr als eine Anweisung. Wenn das Makro in einer if-Anweisung verwendet wird, ist dies problematisch:

if(bIsOk)
   MY_MACRO(42) ;

Dieses Makro kann folgendermaßen erweitert werden:

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

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

Die Funktion g wird unabhängig vom Wert von bIsOk ausgeführt.

Das bedeutet, dass wir dem Makro einen Bereich hinzufügen müssen:

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

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

Produzieren Sie einen gültigen Code 2

Wenn das Makro ungefähr so ​​ist:

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

Wir könnten ein anderes Problem im folgenden Code haben:

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

Weil es sich erweitern würde als:

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

Dieser Code wird natürlich nicht kompiliert. Die Lösung verwendet also einen Gültigkeitsbereich:

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

Der Code verhält sich wieder korrekt.

Kombination von Semikolon + Scope-Effekten?

Es gibt ein C/C++ - Idiom, das diesen Effekt erzeugt: Die do/while-Schleife:

do
{
    // code
}
while(false) ;

Do/while kann einen Gültigkeitsbereich erstellen, wodurch der Code des Makros gekapselt wird, und am Ende wird ein Semikolon benötigt, der zu Code erweitert wird, der einen Code benötigt.

Der Bonus?

Der C++ - Compiler optimiert die do/while-Schleife, da die Tatsache, dass seine Nachbedingung falsch ist, zur Kompilierzeit bekannt ist. Dies bedeutet, dass ein Makro wie:

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

wird sich richtig als erweitern

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

und wird dann als kompiliert und optimiert

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

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

   // Etc.
}
140
paercebal

@ jfm3 - Du hast eine nette Antwort auf die Frage. Sie können auch hinzufügen, dass das Makro-Idiom auch das möglicherweise gefährlichere (weil kein Fehler auftretende) unbeabsichtigte Verhalten mit einfachen 'if'-Anweisungen verhindert:

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

if (test) FOO( baz);

erweitert zu:

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

dies ist syntaktisch korrekt, so dass es keinen Compiler-Fehler gibt, es hat jedoch die wahrscheinlich unbeabsichtigte Folge, dass g() immer aufgerufen wird.

50
Michael Burr

Die obigen Antworten erklären die Bedeutung dieser Konstrukte, es gibt jedoch einen signifikanten Unterschied zwischen den beiden, der nicht erwähnt wurde. In der Tat gibt es einen Grund, den do ... while dem if ... else-Konstrukt vorzuziehen.

Das Problem des if ... else-Konstrukts ist, dass es nicht zwingt Sie, das Semikolon zu setzen. Wie in diesem Code:

FOO(1)
printf("abc");

Obwohl wir das Semikolon (aus Versehen) ausgelassen haben, wird der Code auf erweitert

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

und wird automatisch kompiliert (obwohl einige Compiler eine Warnung wegen nicht erreichbarem Code ausgeben). Die printf-Anweisung wird jedoch niemals ausgeführt.

Das do ... while-Konstrukt hat ein solches Problem nicht, da das einzige gültige Token nach der while(0) ein Semikolon ist.

23
ybungalobill

Während erwartet wird, dass Compiler die do { ... } while(false);-Schleifen optimieren, gibt es eine andere Lösung, die dieses Konstrukt nicht erfordert. Die Lösung ist die Verwendung des Kommaoperators:

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

oder noch exotischer:

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

Dies funktioniert zwar gut mit separaten Anweisungen, funktioniert jedoch nicht mit Fällen, in denen Variablen erstellt und als Teil von #define verwendet werden:

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

Damit müsste man das do/while-Konstrukt verwenden.

15
Marius

Jens Gustedts P99-Präprozessor-Bibliothek (Ja, die Tatsache, dass so etwas existiert, hat mich auch umgehauen!) Verbessert das if(1) { ... } else-Konstrukt auf eine kleine, aber signifikante Weise, indem es Folgendes definiert:

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

Der Grund dafür ist, dass im Gegensatz zum do { ... } while(0)-Konstrukt break und continue immer noch innerhalb des angegebenen Blocks funktionieren, der ((void)0) jedoch einen Syntaxfehler erzeugt, wenn das Semikolon nach dem Makroaufruf ausgelassen wird, da sonst der nächste Block übersprungen würde. (Hier gibt es eigentlich kein "hängendes" Problem, da die else an die nächste if bindet, die sich im Makro befindet.) 

Wenn Sie an den möglichen Dingen interessiert sind, die mit dem C-Präprozessor mehr oder weniger sicher ausgeführt werden können, sollten Sie sich diese Bibliothek ansehen.

10

Aus einigen Gründen kann ich die erste Antwort nicht kommentieren ...

Einige von Ihnen zeigten Makros mit lokalen Variablen, aber niemand hat erwähnt, dass Sie in einem Makro keinen Namen verwenden dürfen! Es wird den Benutzer eines Tages beißen! Warum? Weil die Eingabeargumente in Ihrer Makrovorlage ersetzt werden. Und in Ihren Makrobeispielen verwenden Sie den wahrscheinlich am häufigsten verwendeten variablen Namen i .

Zum Beispiel beim folgenden Makro

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

wird in der folgenden Funktion verwendet

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

das Makro verwendet nicht die beabsichtigte Variable i, die am Anfang von some_func deklariert ist, sondern die lokale Variable, die in der do ... while-Schleife des Makros deklariert ist.

Verwenden Sie daher niemals allgemeine Variablennamen in einem Makro!

6
Mike Meyer

Ich denke nicht, dass es erwähnt wurde

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

würde in übersetzt werden

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

beachten Sie, wie i++ vom Makro zweimal ausgewertet wird. Dies kann zu interessanten Fehlern führen.

3
John Nilsson

Ich finde diesen Trick sehr hilfreich in Situationen, in denen Sie einen bestimmten Wert sequentiell verarbeiten müssen. Wenn auf jeder Verarbeitungsebene ein Fehler oder eine ungültige Bedingung auftritt, können Sie die weitere Verarbeitung vermeiden und frühzeitig ausbrechen. z.B.

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

Erläuterung

do {} while (0) und if (1) {} else sollen sicherstellen, dass das Makro auf nur eine Anweisung erweitert wird. Andernfalls:

if (something)
  FOO(X); 

würde erweitern auf:

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

Und g(X) würde außerhalb der if-Steueranweisung ausgeführt. Dies wird bei Verwendung von do {} while (0) und if (1) {} else vermieden.


Bessere Alternative

Mit einem GNU Anweisungsausdruck (kein Bestandteil von Standard C) haben Sie einen besseren Weg als do {} while (0) und if (1) {} else, um dies zu lösen, indem Sie einfach ({}) verwenden:

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

Und diese Syntax ist mit Rückgabewerten kompatibel (beachten Sie, dass do {} while (0) nicht ist), wie in:

return FOO("X");
2
Cœur

Der Grund, dass do {} while (0) über if (1) {} verwendet wird, besteht darin, dass niemand den Code vor dem Aufruf des Makros do {} while (0) ändern kann, um eine andere Art von Block zu sein. Wenn Sie beispielsweise ein Makro aufgerufen haben, das von if (1) {} umgeben ist:

else
  MACRO(x);

Das ist eigentlich ein else if. Subtiler Unterschied

0
ericcurtin