引数の評価順序。

ありがちダメなコードのダメな理由を書くコーナー。

現象。

#include <stdlib.h>
#include <stdio.h>

int main() {
  printf("puts(\"A\"), puts(\"B\"), puts(\"C\")+puts(\"D\") = %d, %d, %d\n",
         puts("A"), puts("B"), puts("C")+puts("D"));

  return 0;
}

コンパイルして実行すると、

$ gcc -std=c99 -pedantic -O0 -g tmp.c
$ ./a.out
C
D
B
A
puts("A"), puts("B"), puts("C")+puts("D") = 2, 2, 4

と出力された。実は書いている本人も、gccの標準設定ではD C B Aの順に評価されがち、と思っていたのだけど違ったらしい。

説明。

C言語では二項演算子の左右の式や関数に与えた引数はどのような順序で評価されるかを未規程としている。未規程なのでどのような順序で評価されるかは判らないので、上の例は出力が乱れる可能性がある。とは言え言語仕様に反している訳ではないので、何らかの順序で評価されるという点で未定義とは大違いだ。
ちなみに未定義、未規程、実装依存の違いを下のようなもので、実装依存すらほとんど頼りにならないことが分かる。

未定義
どのような動作をするか一切判らない。冗談半分に「鼻から悪魔が出る」という慣用句を使うが、実際にもブルースクリーンでPCが固まるくらいは覚悟しておいた方が良い(エラーも何も出さずに間違った結果を返す方が厄介だけれど)。
未規程
大まかな動作は仕様から予想できるが、詳細については一切判らない。10000回に1回だけ評価順序を反転します、という怖いコンパイラだったとしても知ることはできない。
実装依存
大まかな動作は仕様から予想でき、詳細についてはコンパイラの文書などから知ることができる(はず)。ただし、乱数を用いて順序を決めます、という記述だったらどうするのかという疑問が残る。