do while false イディオム
Java言語やPHP、JavaScript、その他C言語に影響を受けた多くの言語で、do while (0) または、do while (false) という記述を見かけることがあります。
do {
/* do something */
} while (false);
この記述の利用には、break文の活用による「処理の打ち切り」を実現する目的があります。
do {
if (v < 0) break; // 条件に一致した場合はdo while文から抜け出す
printf("変数`v`が`0`以上の場合にのみ表示される");
} while (false);
do {} while (偽)
自体は、繰り返しが行われない処理を意味しており、一見すると無意味で無駄な処理のようにも見えますが、実はこの記述には複数の大きなメリットがあります。
目次
break文で処理を打ち切ることができる
do-while文のブロック内ではbreak文が利用できるため、処理の途中でdo-while文を抜け出す処理を書くことができます。
int value = 9;
do {
if (value == -1) break; // ガード処理
if (value == 0) break;
puts("有効な値です");
} while (false);
この脱出技法には、関数のreturn文による簡潔な記述を模倣する意図があります。
void check() {
if (value == -1) return; // 即時return
if (value == 0) return;
puts("有効な値です");
}
また、do-while falseイディオムの利用には、goto文の利用を回避する効果もあります。
if (value == -1) goto fail;
if (value == 0) goto fail;
puts("有効な値です");
fail:
参考: goto文の活用例と代替手段 #余計な関数化を回避する
余談:応用方法によっては、do {} while (false)
で囲われた処理を、ある種のインライン化されたサブルーチンのようなものとみなすこともできます。もっとも現代ではブロック文やラムダ式、クロージャを用いたほうがより明確な記述となります。
本イディオムの意外な効果
もちろんbreakによる処理の打ち切りを行わなくても、構造化された読みやすいコードを書くことはできます。
if (value != -1 && value != 0) {
puts("有効な値です");
}
ただ、条件式が増えた際には、複雑な記述を招いてしまったり、条件文のネストが深くなってしまうこともあります。
// 内容によっては更に読みづらくなる場合がある
if (!that(value) && value != -1 && value != 0) {
puts("有効な値です");
}
// ネストが深すぎる
if (!that(value)) {
if (value != -1 && value != 0) {
puts("有効な値です");
}
}
// ネストが深すぎる
if (!that(value)) {
if (value != -1) {
if (value != 0) {
puts("有効な値です");
}
}
}
do while falseによる記述はそのような複雑さを排除する目的にも活用できます。
また本イディオムを活用すると、条件文の列挙を実現できるという利点も得られます。このような列挙は、ドキュメント的で読みやすく理解のしやすいコードの実現にも繋がります。
do {
if (that(value)) break;
if (value == -1) break;
if (value == 0) break;
puts("有効な値です");
} while (0);
また、このような条件文列挙の作法は、条件文追加時や削除時に、バージョン管理システム(Git, CVS, Subversion, etc.)に対する余計な競合や紛らわしい差分表示を回避することができるという効果ももたらします。
/* Before */
/* どの条件式が追加されたのか分かりずらい */
[*] |1| if (!that(value) && value != -1 && value != 0) {
|2| puts("有効な値です");
|3| }
/* After */
/* 一行目の条件文が新たに追加されたことがわかる */
[+] |1| if (that(value)) break;
|2| if (value == -1) break;
|3| if (value == 0) break;
|4| puts("有効な値です");
[*]
はコードの変更、[+]
はコードの追加を意味します。変更と追加ではまったく意味が異なるため、今回のような「新たな条件の追加」を意図させたい場合には、After
側のような表現のほうが意味的にも明確となります。
またBefore
側のコードで!that(value)
の追加とvalue != -1
の変更が異なるの開発者によって同時に行われた場合、同一行に対する競合(コンフリクト)が発生することになります。After
側の設計であれば、異なる行の変更となるためマージが容易に行えますが、Before
側では若干手間が掛かります。間違ったマージによってバグを産んでしまう可能性もあります。
ちなみに、Before側の設計でこの問題に対処するためには、条件式毎の改行を必須とすること等が有効な対処となります。
if (!that(value)
&& value != -1
&& value != 0
) {
puts("有効な対処です");
}
if (1 == 1
&& !that(value)
&& value != -1
&& value != 0
) {
puts("マニアックな対処方法です");
}
マクロの問題を回避することができる
C言語やC++のマクロ内では、本イディオムが異なる目的で活用されています。具体的には、マクロ呼び出し記法の末尾にセミコロンの記述を必須とさせ、そしてそのセミコロンの意味をより明確にする目的に利用されています。
#define PRINT(v) do { printf("%d", v); } while (0)
PRINT("hello"); // OK
PRINT("hello") // ← これをエラーにすることができる
if (1) 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); // OK
else
PRINT(2);
本マクロ内で利用されたdo { ... } while (0)
は、文末にセミコロンの記述を必須とします。セミコロンが記述されて初めて、一つの有効な文として解釈されるようになるためです。
よってif-else文内で本マクロを利用しても、記述されたセミコロンが余計な文末として解釈されることもありませんし、逆にセミコロンを省略した場合には適切なエラーが発生することになります。
if (1)
PRINT(1) // error: expected ';' after do/while statement
else
PRINT(2);
このようにdo-while falseイディオムは、関数呼び出し形式のマクロ(m(v);
)を有効な単文として解釈させるための用途としても活用されています。
処理効率への影響
do {} while (false);
という繰り返し文は、while (false);
という余計な条件判断を行っています。
ただ、これによる処理速度の低下を気にする必要はほとんどありません。というのも、コンパイラ言語の場合は、最適化によってそのようなオーバヘッドが回避されることがほとんどだからです。
ただし、JavaScriptやPHP、Perl等のインタプリタ言語の場合は、余計な条件判定が行われる場合もあります。もっともインタプリタ側の構文解析の段階で、最適化が行われる場合もあります。