do {} while (0); の意味と目的【do while false イディオムの利点】

do while false イディオム

Java言語やPHP、JavaScript、その他C言語に影響を受けた多くの言語で、do while (0) または、do while (false) という記述を見かけることがあります。

do {
   /* do something */
} while (false);

この記述の利用には、break文の活用による「処理の打ち切り」を実現する目的があります。

do {} while (偽)自体は、繰り返しが行われない処理を意味しており、一見すると無意味で無駄な処理のようにも見えますが、実はこの記述には複数の大きなメリットがあります。

目次

スポンサーリンク

break文で処理を打ち切ることができる

do-while文のブロック内ではbreak文が利用できるため、処理の途中でdo-while文を抜け出す処理を書くことができます。

int value = 9;
do {
   if (value <= 0) break;
   if (value > 10) break;
   puts("1〜10の数値です");
} while (false);

この脱出技法には、関数のreturn文による簡潔な記述を模倣する意図があります。

void check() {
   if (value <= 0) return; // 即時return
   if (value > 10) return;
   puts("1〜10の数値です");
}

また、do-while falseイディオムの利用には、goto文の利用を回避する効果もあります。

if (value <= 0) goto fail;
if (value > 10) goto fail;
puts("1〜10の数値です");
fail:
参考: goto文の活用例と代替手段 #余計な関数化を回避する
余談: 応用方法によっては、do {} while (false)で囲われた処理を、ある種のインライン化されたサブルーチンのようなものとみなすこともできます。

本イディオムの意外な効果

もちろん、breakによる処理の打ち切りを行わなくても、構造化された読みやすいコードを書くことはできます。

if (value <= 0 && value > 10) {
   puts("1〜10の数値です");
}

ただ、条件式が増えた際には、複雑な記述を招いてしまったり、条件文のネストが深くなってしまうこともあります。

// 内容によっては更に読みづらくなる場合がある
if (!that(value) && value <= 0 && value > 10) {
   puts("1〜10の数値です");
}

// ネストが深すぎる
if (!that(value)) {
   if (value <= 0 && value > 10) {
      puts("1〜10の数値です");
   }
}

// ネストが深すぎる
if (!that(value)) {
   if (value <= 0) {
       if (value > 10) {
          puts("1〜10の数値です");
       }
   }
}

do while falseによる記述はそのような複雑さを排除する目的にも活用できます。

また本イディオムを活用すると、条件文の列挙を実現できるという利点も得られます。このような列挙は、ドキュメント的で読みやすく理解のしやすいコードの実現に繋がります。

do {
   if (!that(value)) break;
   if (value <= 0)   break;
   if (value > 10)   break;
   puts("1〜10の数値です");
} while (0);

また、このような条件文列挙の作法は、条件文追加時や削除時に、バージョン管理システム(Git, CVS, Subversion, etc.)に対する余計な競合や紛らわしい差分表示を回避することができるという効果ももたらします。

/* Before */
// どの条件式が追加されたのか分かりずらい
[*] |1| if (!that(value) && value <= 0 && value > 10) {
    |2|   puts("1〜10の数値です");
    |3| }

/* After */
// 一行目の条件文が新たに追加されたことがわかる
[+] |1| if (!that(value)) break;
    |2| if (value <= 0)   break;
    |3| if (value > 10)   break;
    |4| puts("1〜10の数値です");

[*]はコードの変更、[+]はコードの追加を意味します。変更と追加ではまったく意味が異なるため、今回のような「新たな条件の追加」を意図させたい場合には、After側のような表現のほうが意味的にも明確となります。

またBefore側のコードで!that(value)の追加とvalue > 10の変更が異なるの開発者によって同時に行われた場合、同一行に対する競合(コンフリクト)が発生することになります。After側の設計であれば、異なる行の変更となるためマージが容易に行えますが、Before側では若干手間が掛かります。間違ったマージによってバグを産んでしまう可能性もあります。

ちなみに、Before側の設計でこの問題に対処するためには、条件式毎の改行を必須とすることが有効な対処となります。

if (!that(value)
    && value <= 0
    && value > 10
) {
   puts("1〜10の数値です");
}

マクロの問題を回避することができる

C言語やC++のマクロ内では、本イディオムが異なる目的で活用されています。具体的には、マクロ呼び出し記法の末尾にセミコロンの記述を必須とさせる目的に利用されています。

#define PRINT(v) do { printf("%d", v); } while (0)

PRINT("hello"); // OK
PRINT("hello")  // <-- これをエラーにする目的がある

なぜこのような制限が必要なのかというと、複数の文を扱うマクロと波括弧が省略されたif-else文を組み合わせて利用した際の問題を回避するためです。

複文を扱うマクロを定義する場合、ブロック文({})を用いる事になりますが、この場合、マクロ呼出し時の末尾にセミコロン(;)が記述できなくなる問題を引き起こします。

#define PRINT(v) { printf("%d",  v); fflush(stdout); }

PRINT(99); // この場合は問題ない

if (0)
   PRINT(1); // <-- 構文エラー: Expected expression
else
   PRINT(2);

複文が利用されたマクロをif-else文で用いると、セミコロンが余計な文末として解釈されてしまうのです。

if (0)
   { printf("%d",  1); fflush(stdout); }; // このセミコロンが余計(コンパイルエラーに繋がる)
else
   { printf("%d",  2); fflush(stdout); };

この問題に対処するためには、do while 0による表現を用いる必要があります。

#define PRINT(v) do { printf("%d",  v); fflush(stdout); } while (0)

if (1)
   PRINT(1);
else         // OK
   PRINT(2);

本マクロ内で利用されたdo { ... } while (0)は、文末にセミコロンの記述を必須とします。セミコロンが記述されて初めて、一つの有効な文として解釈されるようになるためです。

そのためif-else文内で本マクロを利用すれば、記述したセミコロンが余計な文末として解釈されることもありません。

処理効率への影響

do {} while (false);という繰り返し文は、while (false);という余計な条件判断を行っています。

ただ、これによる処理速度の低下を気にする必要はほとんどありません。というのも、コンパイラ言語の場合は、最適化によってそのようなオーバヘッドが回避されることがほとんどだからです。

ただし、JavaScriptやPHP、Perl等のインタプリタ言語の場合は、余計な条件判定が行われる場合もあります。もっともインタプリタ側の構文解析の段階で、最適化が行われる場合もあります。

広告