もう5年以上C言語でコードを書いていて、しかも今ではそれで収入を得ている身だというのに、実は今まで「A && B || C」がどう評価されるのかイマイチ確信を持てていなかった。
いや、Cの論理演算子は左から右に順に評価していくとか短絡評価されるとか、その辺りは理解している。
ただC言語の仕様に基づく解釈として、例えば「A && B || C」と書いた時の評価結果は、
- 「(A && B) || C」
- 「A && (B || C)」
このどれと等しいのか? 「優先度は '||' よりも '&&' の方が高いので、1番目の解釈になるのだろうか?」と思いつつ、特に調べるわけでもなく放置していた。
で、あやふやなまま過ごしていたら、つい最近になってとあるソースコード中に「A && B || C」な部分があって、TDM-GCCでコンパイルしたら警告が出た。
warning: suggest parentheses around '&&' within '||'
要は「適宜括弧を補いましょう」ということだよね?
この警告を見て「謎を解明すべき時が遂に来た!」と悟った次第で、まず試しに幾つかの処理系の挙動を調べてみることにした。こんなテストコードを用意。
#include <stdio.h> static int a(void) { (void) puts("a"); return 1; } static int b(void) { (void) puts("b"); return 1; } static int c(void) { (void) puts("c"); return 1; } int main(void) { (void) (a() && b() || c()); return 0; }
関数a〜cの戻り値の値を変えてみて、どの関数がどの順番で実行されるか表示させてみた。
実験した処理系は以下の通り。
処理系 | OS |
---|---|
TDM-GCC 4.5.1 | Windows XP SP3 |
Microsoft Visual Studio 2005 SP1 | 同上 |
Microsoft Visual Studio 2010 | 同上 |
Digital Mars C/C++ Compiler 8.49 | 同上 |
GCC 4.2.1 | FreeBSD 8.1-RELEASE |
GCC 4.3.2 | Debian GNU/Linux 5.0 |
どの処理系を使っても、結果はこの通り。1とか0は、各関数の戻り値。
a() | b() | c() | 結果 |
---|---|---|---|
1 | 1 | 1 | a -> b |
1 | 0 | 1 | a -> b -> c |
0 | 1 | 1 | a -> c |
最後のテスト結果から推測するに、「A && B || C」と書くと「(A && B) || C」と同じように評価されるようだ。
とはいえ手近な処理系の挙動を見ただけなので、「結局、仕様的にはどうなの?」という疑問は晴れないままだったりする。
調べてみたら『S・P・ハービソン3世とG・L・スティール・ジュニアのCリファレンスマニュアル』のP.268にこんな記述があった。
(省略)&& は || より優先順位が高いが,括弧を使う方が読みやすくなることが多い. 例 a<b || b<c && c<d || d<e は,次のように書く方が明快である. a<b || (b<c && c<d) || d<e
ということは、仕様的にも「(A && B) || C」と解釈されるのが正解なのだろうか?
ところで、何で今まで理解が曖昧なままでも大丈夫だったかというと、適宜括弧を補ってコードを書いていたからだ。何しろその方がコンパイラにも他のプログラマにも私の意図が伝わりやすいので。仕事でコードを書いていると、そんなものだ。
さて、エントリ名の「『A && B || C』の評価結果に気をつけろ」だが……件のコードはどう考えても「A && (B || C)」と評価されなければならないのだが、括弧が無いので「(A && B) || C」と同じように評価されてしまう訳で、有体に言えばバグっているのだ。