マクロで明らかに無意味なdo-whileおよびif-elseステートメントを使用するのはなぜですか?


788

多くのC / C ++マクロで、無意味なdo whileループのように見えるマクロに囲まれたマクロのコードが表示されます。以下に例を示します。

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

私は何をしdo whileているのか分かりません。これなしでこれを書いてみませんか?

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

2
elseの例ではvoid、最後にtypeの式を追加します... ((void)0)のようにします。
Phil1970

1
do whileコンストラクトはreturnステートメントと互換性がないため、if (1) { ... } else ((void)0)標準C ではコンストラクトに互換性のある使用法があることに注意してください。GNUCでは、私の回答に記載されているコンストラクトを優先します。
・クール

回答:


829

do ... whileそしてif ... else、あなたのマクロの後にセミコロンが常に同じことを意味するようにそれを作るためにあります。2番目のマクロのようなものがあったとしましょう。

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

あなたが使用した場合、今BAR(X);ではif ... elseif文の体が中括弧に包まれていなかった声明、あなたが悪い驚きを得ると思います。

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つの方法があります。1つ目は、コンマを使用して、式のように機能する機能を奪わずに、マクロ内のステートメントをシーケンス処理することです。

#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、それが「につながるぶら下がり他に、次のコードのように、既存のダングリング他の問題はさらに困難を見つけるために作ることができました、」 。

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

ポイントは、ぶら下がっているセミコロンが誤っている状況でセミコロンを使い果たすことです。もちろん、この時点でBAR、マクロではなく実際の関数として宣言する方がよいと主張することもできます(おそらくそうすべきです)。

要約するdo ... whileと、Cプリプロセッサの欠点を回避するためにあります。それらのCスタイルガイドがCプリプロセッサを解雇するように指示するとき、これは彼らが心配しているようなものです。


18
これは、if、while、forステートメントで常に中かっこを使用するという強力な議論ではありませんか?これを常に行うようにすると(たとえば、MISRA-Cで必要になるように)、上記の問題は解消されます。
スティーブ・メルニコフ2009

17
カンマの例は、#define BAR(X) (f(X), g(X))そうでなければ演算子の優先順位がセマンティクスを台無しにする可能性があります。
スチュワート

20
@DawidFerenczy:あなたと私(4年半前)の両方が良い点を示していますが、私たちは現実の世界に住んでいる必要があります。ifコード内のすべてのステートメントなどで中かっこが使用されていることを保証できない場合を除き、このようにマクロをラップすることは問題を回避する簡単な方法です。
Steve Melnikoff 2013年

8
注:if(1) {...} else void(0)フォームはdo {...} while(0)、breakまたはcontinueキーワードの動作を変更しないため、パラメーターがマクロ展開に含まれるコードであるマクロよりも安全です。例:ブレークがマクロ呼び出しサイトでforループではなくマクロのwhileループに影響するため、が定義されているfor (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }場合、予期しない動作が発生します。MYMACRO#define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)
Chris Kline

5
@ace void(0)はタイプミスでした(void)0。そして、これで「ダングリングエルサー」の問題解決すると思います(void)0。の後にセミコロンがないことに注意してください。その場合、他のダングリング(たとえばif (cond) if (1) foo() else (void)0 else { /* dangling else body */ })はコンパイルエラーをトリガーします。ここだ、それを実証する実際の例では
クリス・クライン

153

マクロは、プリプロセッサが正規のコードに挿入するテキストのコピー/貼り付け部分です。マクロの作成者は、置き換えによって有効なコードが生成されることを期待しています。

これを成功させるには、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.
}

6
マクロをインライン関数に変更すると、いくつかの標準の定義済みマクロが変更されることに注意してください。たとえば、次のコードはFUNCTIONおよびLINEの変更を示しています。#include <stdio.h> #define Fmacro()printf( "%s%d \ n"、FUNCTIONLINE)inline void Finline(){printf( "%s%d \ n"、FUNCTIONLINE); } int main(){Fmacro(); Finline(); 0を返します。}(太字の用語は二重アンダースコアで囲む必要があります
不適切

6
この回答には、重要ではないが完全に重要でない問題がいくつかあります。たとえば:void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }は正しい展開ではありません。x拡張では、より複雑な問題が拡大されるものです32. AでなければなりませんMY_MACRO(i+7)。もう1つはの拡張ですMY_MACRO(0x07 << 6)。良い点はたくさんありますが、ドットなしのiとクロスしていないtがあります。
ジョナサンレフラー2013

@Gnubie:あなたがまだここにいて、これまでにこれを理解していない場合:バックスラッシュを使用してコメントのアスタリスクとアンダースコアをエスケープできるため、タイプ\_\_LINE\_\_すると__LINE__としてレンダリングされます。私見、コードのフォーマットをコードに使用する方が良いです。たとえば、__LINE__(特別な処理は必要ありません)。PS 2012年にこれが当てはまったかどうかはわかりません。それ以来、彼らはエンジンにかなりの改良を加えてきました。
スコット

1
私のコメントは6年遅れであることに感謝しますが、ほとんどのCコンパイラは実際にはinline関数をインライン化していません(標準で許可されています)
Andrew

53

@ jfm3-あなたは質問に対して素晴らしい答えがあります。また、マクロイディオムは、単純な 'if'ステートメントを使用して、(エラーが発生しないため)潜在的に危険な意図しない動作を防止することも追加できます。

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

if (test) FOO( baz);

展開する:

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

これは構文的に正しいため、コンパイラエラーは発生しませんが、g()が常に呼び出されるという意図しない結果になる可能性があります。


22
「おそらく意図しない」?私は「確かに意図しない」と言っていただろうか、さもなければプログラマーは(鞭で丸く懲らされるのではなく)連れ出されて撃たれる必要があります。
Lawrence Dol

4
それとも、彼らは3文字の機関に勤務し、ひそかに広く使われているオープンソースプログラム... :-)にそのコードを挿入している場合、昇給を与えられるかもしれない
R .. GitHubのSTOPはICE手助け

2
そして、このコメントは、Apple OSで発見された最近のSSL証明書検証バグのgoto fail行を思い出させてくれます
Gerard Sexton 14年

23

上記の回答はこれらの構成要素の意味を説明していますが、言及されていない2つの構成要素には大きな違いがあります。実際には、好む理由があるdo ... whileif ... else構築物。

構成の問題は、セミコロンを置くことを強制if ... elseしないことです。このコードのように:

FOO(1)
printf("abc");

セミコロンは(誤って)省略していますが、コードは次のように展開されます。

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

サイレントコンパイルします(ただし、一部のコンパイラは到達できないコードに対して警告を発行する場合があります)。ただし、printfステートメントは実行されません。

do ... whileの後に有効なトークンwhile(0)はセミコロンだけなので、構文にはそのような問題はありません。


2
@RichardHansen:マクロの呼び出しを見ると、それがステートメントに展開されているのか、式に展開されているのかわからないため、それでもそれほど良くはありません。誰かが後者を想定している場合、彼女は書くかもしれませFOO(1),x++;ん。使用するだけdo ... whileです。
Yakov Galka

1
誤解を避けるためにマクロを文書化することで十分です。これdo ... while (0)は望ましいことだと私は同意しますが、欠点は1つあります。マクロの呼び出しを含むループではなく、breakまたはcontinuedo ... while (0)ループを制御します。したがって、ifトリックにはまだ価値があります。
Richard Hansen、

2
マクロの疑似ループ内でa breakまたはa continueを配置できる場所がわかりませんdo {...} while(0)。マクロパラメータでも構文エラーになります。
PatrickSchlüter

5
コンストラクトのdo { ... } while(0)代わりに使用するもう1つの理由if whateverは、その慣用的な性質です。do {...} while(0)構築物は、よく知られており、多くのプログラマによって多くを使用、普及しています。その理論的根拠とドキュメントはすぐにわかります。if構成についてはそうではありません。そのため、コードのレビューを行うときに手間がかかりません。
PatrickSchlüter

1
@tristopia:人々がコードのブロックを引数として取るマクロを書くのを見てきました(これは必ずしもお勧めしません)。次に例を示します#define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0。のようCHECK(system("foo"), break;);に使用できます。break;は、CHECK()呼び出しを囲むループを参照することを目的としています。
Richard Hansen

16

コンパイラーは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構文の使用が強制されます。


おかげで、コンマ演算子は実行順序を保証しないので、このネストはそれを強制する方法です。
Marius

15
@マリウス:偽; カンマ演算子はシーケンスポイントであるため、実行順序保証されます。関数の引数リストのコンマと混同しているようです。
R .. GitHub ICE HELPING ICE STOP 2012

2
2番目のエキゾチックな提案が私の一日を作りました。
Spidey

コンパイラがプログラムの監視可能な動作を維持することを強制されるため、do / while awayの最適化は大したことではありません(コンパイラの最適化が正しいと想定)。
マルコ

@MarcoA。あなたが正しい間、私は過去に、コードの機能を正確に維持しながらコンパイラの最適化を見つけましたが、単一のコンテキストでは何もしないように見える行を変更することにより、マルチスレッドアルゴリズムが壊れます。適切な事例Peterson's Algorithm
マリウス

11

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)構成とは異なり、与えられたブロック内breakcontinueも機能((void)0)しますが、マクロの呼び出しの後にセミコロンを省略すると、構文エラーが発生します。そうしないと、次のブロックがスキップされます。(実際には、ここでは「ぶら下がるelse」の問題はありません。これは、マクロ内のelse最も近いifにバインドしているためです。)

Cプリプロセッサで多かれ少なかれ安全に実行できる種類のことに興味があるなら、そのライブラリをチェックしてください。


非常に賢明ですが、これにより、他のぶら下がりの可能性に関するコンパイラの警告が発生します。
2015年

1
通常、マクロを使用して包含環境を作成します。つまり、マクロ内でbreak(またはcontinue)を使用して外部で開始/終了したループを制御することはありません。
mirabilos 2016

Boostにはプリプロセッサライブラリもあります。それについて何が驚くべきことですか?
Rene

のリスクelse ((void)0)は、誰かが書いている可能性がYOUR_MACRO(), f();あり、構文的には有効ですが、を呼び出さないことですf()。これdo whileは構文エラーです。
メルポメン

@melpomeneはどうelse do; while (0)ですか?
カール・レイ

8

いくつかの理由で、最初の回答についてコメントすることはできません...

ローカル変数を使用してマクロを表示した人もいましたが、マクロで名前を使用するだけではだめだと言った人はいませんでした。いつかユーザーを噛みます!どうして?入力引数がマクロテンプレートに代入されるためです。また、マクロの例では、おそらく最も一般的に使用される変数名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ループで宣言されているローカル変数を使用します。

したがって、マクロでは一般的な変数名を使用しないでください。


通常のパターンは、たとえばマクロの変数名にアンダースコアを追加することint __i;です。
Blaisorblade、2011

8
@Blaisorblade:実際、それは正しくない、違法なCです。先頭のアンダースコアは、実装による使用のために予約されています。この「通常のパターン」を見た理由は、システムヘッダー(「実装」)を読み取るためであり、この予約された名前空間に制限される必要があります。アプリケーション/ライブラリの場合は、アンダースコアなどを使用せずに、衝突する可能性の低い独自のあいまいな名前を選択する必要がありますmylib_internal___i
R .. GitHub ICEのヘルプ停止2012

2
@R ..そうです-私は実際にこれを「アプリケーション」、Linuxカーネルで読みましたが、標準ライブラリを使用していないため、とにかく例外です(厳密には、「独立した」C実装です) 「ホストされた」もの)。
Blaisorblade、2012

3
@R ..これはまったく正しくありません。先頭のアンダースコアの後に大文字または2番目のアンダースコア続くものは、すべてのコンテキストでの実装用に予約されています。先行アンダースコアの後に何かが続くものは、ローカルスコープで予約されていません。
Leushenko、2014年

@Leushenko:はい、しかしその区別は十分に微妙なので、そのような名前をまったく使用しないように人々に伝えるのが最善だと私は思います。繊細さを理解している人たちは、おそらく私が詳細をぼかしていることをすでに知っているでしょう。:-)
R .. GitHub ICEのヘルプ停止

7

説明

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");

3
マクロでブロッククランプ{}を使用すれば、マクロコードをバンドルして、すべてが同じif条件パスに対して実行されるようになります。do-whileアラウンドは、マクロが使用される場所でセミコロンを強制するために使用されます。したがって、マクロはより多くの機能を同様に動作するように強制されます。これには、使用する場合の末尾のセミコロンの要件が含まれます。
Alexander Stohr

2

言及されたとは思わないので、これを検討してください

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

に翻訳されます

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

i++マクロによって2回評価される方法に注意してください。これにより、いくつかの興味深いエラーが発生する可能性があります。


15
これは、do ... while(0)構文とは関係ありません。
トレント

2
本当です。ただし、マクロと関数のトピック、および関数として動作するマクロの記述方法に関連しています...
John Nilsson

2
上記と同様、これは回答ではなくコメントです。トピックに関する:あなたは一度だけのものを使用する理由だこと:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
mirabilos
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.