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)
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.
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:
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.
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) ;
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.
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.
}
@ 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.
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.
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.
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.
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!
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.
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);
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.
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");
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