マクロで明らかに意味のないdo-whileおよびif-elseステートメントを使用するのはなぜですか?
-
03-07-2019 - |
質問
多くのC / C ++マクロでは、マクロのコードが無意味な do while
ループに包まれているように見えます。以下に例を示します。
#define FOO(X) do { f(X); g(X); } while (0)
#define FOO(X) if (1) { f(X); g(X); } else
実行中に
が何をしているのかわかりません。なぜこれなしでこれを書くだけではどうですか?
#define FOO(X) f(X); g(X)
解決
do ... while
および if ... else
は、
マクロの後のセミコロンは常に同じことを意味します。言ってみよう
2番目のマクロのようなものがありました。
#define BAR(X) f(x); g(x)
Now if BAR(X);
を if ... else
ステートメントで使用する場合、ifステートメントの本体が中括弧で囲まれていない場合、あなたはひどい驚きを得るでしょう。
if (corge)
BAR(corge);
else
gralt();
上記のコードは次のように展開されます
if (corge)
f(corge); g(corge);
else
gralt();
elseはifに関連付けられていないため、は構文的に正しくありません。中括弧の後のセミコロンは構文的に間違っているため、マクロ内で中括弧で物事をラップすることは役に立ちません。
if (corge)
{f(corge); g(corge);};
else
gralt();
問題を修正するには2つの方法があります。最初の方法は、コンマを使用して、式のように機能する能力を奪うことなく、マクロ内のステートメントをシーケンスすることです。
#define BAR(X) f(X), g(X)
上記のバー BAR
は、上記のコードを次のように展開します。これは構文的に正しいです。
if (corge)
f(corge), g(corge);
else
gralt();
これは、 f(X)
の代わりに、たとえばローカル変数を宣言するなど、独自のブロックに入れる必要があるより複雑なコード本体がある場合には機能しません。最も一般的な場合の解決策は、 do ... while
のようなものを使用して、マクロを混乱せずにセミコロンを取る単一のステートメントにすることです。
#define BAR(X) do { \
int i = f(X); \
if (i > 4) g(i); \
} while (0)
do ... while
を使用する必要はありませんが、 if ... else
で何かを調理することもできますが、 if ... else
は if ... else
内で展開され、" dangling else "を使用すると、次のコードのように、既存のdangling elseの問題をさらに見つけにくくすることができます。
if (corge)
if (1) { f(corge); g(corge); } else;
else
gralt();
ポイントは、ぶら下がりセミコロンが間違っているコンテキストでセミコロンを使い果たすことです。もちろん、この時点で、 BAR
をマクロではなく実際の関数として宣言する方が良いと主張することができます(おそらくそうすべきです)。
要約すると、 do ... while
は、Cプリプロセッサの欠点を回避するためにあります。これらのCスタイルガイドがCプリプロセッサをレイオフするように指示するとき、これは彼らが心配しているようなものです。
他のヒント
マクロは、プリプロセッサが本物のコードに入れるテキストのコピー/貼り付けです。マクロの作成者は、置換によって有効なコードが生成されることを望んでいます。
3つの「ヒント」があります。それで成功するには:
マクロが本物のコードのように振る舞うようにする
通常、通常のコードはセミコロンで終了します。ユーザーがコードを表示する必要がない場合...
doSomething(1) ;
DO_SOMETHING_ELSE(2) // <== Hey? What's this?
doSomethingElseAgain(3) ;
これは、セミコロンがない場合にコンパイラがエラーを生成することをユーザーが期待することを意味します。
しかし、本当の本当の正当な理由は、いつか、マクロの作者がおそらくマクロを純粋な関数(おそらくインライン化された)で置き換える必要があるということです。したがって、マクロは本当に動作するはずです。
したがって、セミコロンが必要なマクロが必要です。
有効なコードを生成する
jfm3の回答に示されているように、マクロに複数の命令が含まれている場合があります。マクロがifステートメント内で使用されている場合、これは問題になります:
if(bIsOk)
MY_MACRO(42) ;
このマクロは次のように展開できます:
#define MY_MACRO(x) f(x) ; g(x)
if(bIsOk)
f(42) ; g(42) ; // was MY_MACRO(42) ;
g
関数は、 bIsOk
の値に関係なく実行されます。
これは、マクロにスコープを追加する必要があることを意味します。
#define MY_MACRO(x) { f(x) ; g(x) ; }
if(bIsOk)
{ f(42) ; g(42) ; } ; // was MY_MACRO(42) ;
有効なコード2を生成
マクロが次のようなものである場合:
#define MY_MACRO(x) int i = x + 1 ; f(i) ;
次のコードで別の問題が発生する可能性があります:
void doSomething()
{
int i = 25 ;
MY_MACRO(32) ;
}
次のように展開されるため:
void doSomething()
{
int i = 25 ;
int i = 32 + 1 ; f(i) ; ; // was MY_MACRO(32) ;
}
もちろん、このコードはコンパイルされません。したがって、再び、ソリューションはスコープを使用しています:
#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) ;
}
コードは再び正常に動作します。
セミコロン+スコープエフェクトの組み合わせ?
この効果を生み出すC / C ++イディオムが1つあります:do / whileループ:
do
{
// code
}
while(false) ;
do / whileはスコープを作成できるため、マクロのコードをカプセル化し、最後にセミコロンが必要になるため、必要なコードに拡張します。
ボーナスですか?
C ++コンパイラは、後処理条件がfalseであるという事実がコンパイル時にわかっているため、do / whileループを最適化します。これは、次のようなマクロを意味します:
#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.
}
として正しく展開します
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.
}
次にコンパイルされ、次のように最適化されます
void doSomething(bool bIsOk)
{
int i = 25 ;
if(bIsOk)
{
f(43) ; g(43) ;
}
// Etc.
}
@ jfm3-質問に対する良い答えがあります。また、マクロイディオムは、単純な 'if'ステートメントを使用した、より危険な(エラーがないため)意図しない動作を防ぐことも追加できます。
#define FOO(x) f(x); g(x)
if (test) FOO( baz);
展開先:
if (test) f(baz); g(baz);
これは構文的に正しいため、コンパイラエラーはありませんが、g()が常に呼び出されるという意図しない結果になる可能性があります。
上記の回答はこれらの構造の意味を説明していますが、言及されていない2つの間には大きな違いがあります。実際、 if ... else
構造よりも do ... while
を好む理由があります。
if ... else
構造の問題は、セミコロンを挿入することを強制しないことです。このコードのように:
FOO(1)
printf("abc");
(誤って)セミコロンを省略しましたが、コードは次のように展開されます
if (1) { f(X); g(X); } else
printf("abc");
そして静かにコンパイルします(ただし、一部のコンパイラは到達不能コードに対して警告を発行する場合があります)。ただし、 printf
ステートメントは実行されません。
while(0)
の後の唯一の有効なトークンはセミコロンであるため、 do ... while
構造にはこのような問題はありません。
コンパイラーは do {...} while(false);
ループを最適化することが期待されますが、その構成を必要としない別のソリューションがあります。解決策は、コンマ演算子を使用することです:
#define FOO(X) (f(X),g(X))
またはさらにエキゾチックに:
#define FOO(X) g((f(X),(X)))
これは個別の命令でうまく動作しますが、変数が #define
の一部として構築され使用される場合には動作しません:
#define FOO(X) (int s=5,f((X)+s),g((X)+s))
これにより、do / whileコンストラクトの使用が強制されます。
Jens Gustedtの P99プリプロセッサライブラリ(そういうものが存在するという事実は、以下も定義することにより、小さいながらも重要な方法で if(1){...} else
構造を改善します。
#define P99_NOP ((void)0)
#define P99_PREFER(...) if (1) { __VA_ARGS__ } else
#define P99_BLOCK(...) P99_PREFER(__VA_ARGS__) P99_NOP
この理由は、 do {...} while(0)
コンストラクトとは異なり、 break
および continue
が引き続き機能することです指定されたブロック内で、マクロ呼び出しの後にセミコロンが省略されると((void)0)
は構文エラーを作成します。そうしないと、次のブロックをスキップします。 ( else
はマクロ内の最も近い if
にバインドするため、実際には「ぶら下がるelse」の問題はありません。)
Cプリプロセッサで多かれ少なかれ安全に実行できる種類のことに興味がある場合は、そのライブラリをチェックしてください。
何らかの理由で、最初の回答についてコメントできない...
一部のユーザーは、ローカル変数を使用してマクロを表示しましたが、マクロで名前を使用することはできないと言う人はいませんでした!それはいつかユーザーを噛むでしょう!どうして?入力引数がマクロテンプレートに代入されるためです。マクロの例では、おそらく最も一般的に使用される変数名 i を使用しています。
たとえば、次のマクロの場合
#define FOO(X) do { int i; for (i = 0; i < (X); ++i) do_something(i); } while (0)
次の関数で使用されます
void some_func(void) {
int i;
for (i = 0; i < 10; ++i)
FOO(i);
}
マクロは、some_funcの先頭で宣言されている目的の変数iを使用せず、マクロのdo ... whileループで宣言されているローカル変数を使用します。
したがって、マクロでは一般的な変数名を使用しないでください!
説明
do {} while(0)
および if(1){} else
は、マクロが1命令のみに展開されるようにするためのものです。それ以外の場合:
if (something)
FOO(X);
展開先:
if (something)
f(X); g(X);
そして g(X)
は、 if
制御ステートメントの外側で実行されます。 do {} while(0)
および if(1){} else
を使用する場合、これは回避されます。
より良い代替
GNUを使用ステートメント式(標準Cの一部ではない)、 do {} while(0)
および if(1){} else
を解決するよりも良い方法がありますこれは、単に({})
を使用して:
#define FOO(X) ({f(X); g(X);})
そして、この構文は戻り値と互換性があります( do {} while {0)
はそうではないことに注意してください)。
return FOO("X");
言及されたとは思わないので、これを考慮してください
while(i<100)
FOO(i++);
に翻訳されます
while(i<100)
do { f(i++); g(i++); } while (0)
マクロによって i ++
が2回評価されることに注意してください。これはいくつかの興味深いエラーにつながる可能性があります。
このトリックは、特定の値を順番に処理する必要がある場合に非常に役立つことがわかりました。処理の各レベルで、エラーまたは無効な状態が発生した場合、それ以上の処理を避けて早期に脱出できます。例:
#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);