web-dev-qa-db-ja.com

どうしてマクロで無意味なdo-whileやif-else文を使わないのですか?

多くの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)
725
jfm3

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

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

Ifステートメントの本体が中括弧で囲まれていないif ... elseステートメントでBAR(X);を使用する場合、ひどい驚きがあります。

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 ... elseif ... elseの内部に展開すると、「- dangling else "。これにより、次のコードのように、既存のdangling else問題を見つけるのがさらに難しくなります。

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

ポイントは、ぶら下がりセミコロンが間違っているコンテキストでセミコロンを使い切ることです。もちろん、この時点でBARをマクロではなく実際の関数として宣言する方が良いと主張することができます(おそらくそうすべきです)。

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

781
jfm3

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

そのためには、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++コンパイラは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.
}
143
paercebal

@ jfm3 - 質問にいい答えがあります。また、マクロの慣用句が(エラーがないために)より危険な可能性のある意図しない動作を単純な 'if'ステートメントで防ぐことを追加したい場合があります。

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

if (test) FOO( baz);

に展開:

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

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

51
Michael Burr

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

if ... else構文の問題は、それが強制あなたがセミコロンを置かないということです。このコードのように:

FOO(1)
printf("abc");

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

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

そして黙ってコンパイルします(ただし、一部のコンパイラは到達不能コードに対して警告を発行する場合があります)。しかしprintfステートメントは決して実行されません。

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

23
ybungalobill

コンパイラは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構文を使わざるをえなくなるでしょう。

16
Marius

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はマクロ内の最も近いifにバインドされるため、ここでは実際には「ぶら下がるelse」問題はありません。)

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

10

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

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

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

6
Mike Meyer

説明

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
Cœur

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

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

に翻訳されます

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

マクロによってi++が2回評価される様子に注目してください。これはいくつかの興味深いエラーにつながる可能性があります。

2
John Nilsson

このトリックは、あなたが特定の値を順番に処理しなければならないような状況で非常に役立つことがわかりました。各処理レベルで、何らかのエラーや無効な状態が発生した場合は、それ以上の処理を避けて早めに処理を進めることができます。例えば.

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

do {} while (0)if (1) {}よりも使用されるのは、マクロdo {} while (0)を呼び出して他の種類のブロックにする前に誰かがコードを変更することができないからです。たとえば、if (1) {}で囲まれたマクロを次のように呼び出したとします。

else
  MACRO(x);

それは実際にはelse ifです。微妙な違い

0
ericcurtin