o-taki氏からのタレコミです。
まずはC言語。
問題のコードは下記です。
#include <stdio.h>
int main(int argc, int *argv[]){
int a[2][2] = {{6,8},{100,120}}; // ①
int *t = *a; // ②
int **l = &t-1; // ③
printf("%d\n",++*++*++l*++*++*l++); // ④
return 0;
}
まあ、②、③のあたりでやや意味不明ですが、要はlはtのアドレスの一個手前だからaの一個手前、つまり何もないところ指してる。多分、直接**lにアクセスすると怒られると思われます。
④の所を解析すると、演算子の優先順位を考えると
++*++*++l*++*++*l++
= ++*++(*(++l)) * ++*++(*(l++))
= (++(*(++(*(++l))))) * (++(*(++(*(l++)))))
= (++(*(++(*(++l))))) * (++(*(++(*(l++))))) // ⑤
になるんじゃないかと思います。ややこしいことは間違いないですが、問題は「後置インクリメントがどのタイミングで評価されるのか」ということに集約されると思います。
まず、⑤の最初の大きな括弧
(++(*(++(*(++l))))) // ⑤-i
が最終的にどこを指しているか考えます。a[][]のメモリ領域を便宜的に下記の表現すると、
Z.a:0 Z.b:0
A.a:6 A.b:8
B.a:100 B.b:120
初期状態でlはZ(=Z.a)を指しています。順を追うと、
(++l) => A (=A.a)
(*(++l)) => A.a
(++(*(++l))) => A.b // ⑥
</code></pre>
したがって最後の
<pre><code>
(++(*(++(*(++l)))))
でA.bの値が8から9に変更されます。
注意すべきは、最後の時点でlが指している先はA.bです。(多分)
⑥のインクリメントは*lに対するインクリメントですが、*lもlも一緒な気がします。
で、⑤の二つめの大きな括弧(これが問題)
(++(*(++(*(l++))))) // ⑤-ii
も同様に考えると
(l++) => A (=A.b) // ⑦
(*(++l)) => A.b
(++(*(++l))) => B.a
というわけで、⑦は後置のため、参照される段階ではまだインクリメントされません。(多分)
したがって最後の
(++(*(++(*(l++)))))
でB.aの値が100から101に変更されます。
ということは、④=⑤=
(++(*(++(*(++l))))) * (++(*(++(*(l++)))))
は9*101=909になります。
といいたいところなんですけど、コンパイラの実装依存ぽい感があります。まあ、上の解析が間違っている可能性も大ですが、最初のmainをコンパイルして実行すると、
・cygwin gcc ⇒ 909
・cygwin g++ ⇒ 909
・VC6 ⇒ 909
・VC2005 ⇒ 10201
謎です。VC2005。
これを解釈するには、二項演算子*は最後に評価されるため、前半の括弧(⑤-i)と後半の括弧(⑤-ii)の前置インクリメントが評価された後のlを考えれば、説明が付くような、付かないような・・・。
次にPerl。
超単純な次のコード。
$i = 1;
print ++$i + $i++;
print "\n";
$i = 1;
print $i++ + ++$i;
print "\n";
$i = 1;
print $i++ + $i++;
最初の出力は後置インクリメントなわけですから、2+2=4と表示されると思われたんですが結果は5。謎。
上記結果は下記のようになります。
5
4
3
全く謎。
Cでやると
i = 1;
printf("%d\n", ++i + i++);
i = 1;
printf("%d\n", i++ + ++i);
i = 1;
printf("%d\n", i++ + i++);
もちろん結果は
4
4
2
よって、結論!
インクリメントは使ってはならない。ルビーのように。
追記:2009-04-27
匿名さんのコメントより
>少なくともC/C++言語仕様上、副作用完了点の前に同じ変数を複数回変更した結果は「未定義(undefined)」です。
ということらしいです。他の言語についてまだ調べてませんが、言語使用上は動作を規定する義務はないので、どうなろうが知ったこっちゃないという。そうだったのか・・・。
まあ結論は同じで、そんな変なインクリメントをするなってことだと思います。
以上
i = 1;
printf("%d\\n", i++ + ++i);
が4になることを考えると、VC2005のように前置インクリメント演算子はすべて最初に評価されるのが正しくて、つまり、gcc・VC6はバグってるということかな?
ちなみにPHPでも上記コードは4になります。
i++ + ++i
処理を見ていくと
1)i++ i=2になるが後置なので返されるのはインクリメント前の1
2)++i i=3
3) 1)の結果1 + 2)の結果2 でi=4
不思議はありません
3)書き間違えてます 2)の結果3です
>通りすがりさん
i++ + ++i
が4になるのはおっしゃる通りの説明で良いと思っています。これ自体は何も不思議ではありません。
i++ で1が返って、++iで3が返って合計 4ということになると、Perlで
print $i++ + $i++;
が3となるのは一つ目で1が返って$iが2になり、二つ目で2が返る($iは3になる)、ということで納得できます。
が、それだとCの
printf(“%dn”, i++ + i++);
で2になるのがどういうことかわかりません。
これが2になるためには後置はすべての計算の後で評価されると考える必要があります。つまり、一つ目も二つ目も1を返し、その後iは3になっている。
この辺りが言語や実装に依存する部分だといっているつもりです。
あと、どうしようもないのは
print ++$i + $i++;
が5となることです。
$i++が3を返しているとしか思えない。
末尾に後置インクリメントを置いたときはインクリメントが先に評価されるようで、これってバグじゃないの?としか思えません。
ピンバック: clipfog.dyndns.org » なるようになる
少なくともC/C++言語仕様上、副作用完了点の前に同じ変数を複数回変更した結果は「未定義(undefined)」です。
前置インクリメントと後置インクリメントの順番とかはどうでもよく、
未定義動作の時点でその結果は保証されませんから、
コンパイラ毎に挙動が異なっても何も問題ありません。
C/C++においては、実装依存(implementation-defined)ですらありませんので、
未定義動作を引き起こすような無作法なコードを書いたプログラマの責任です。
追伸
C/C++の言語仕様(ISO/IEC9899, ISO/IEC14882)には、”implementation-defined”という用語定義がありますが、
この現象はこれに該当しないため、タイトルが不適切ではないかと思います。
また、インクリメントの問題でもなく副作用の問題なので、この意味でも好ましくないかもしれません。
C/C++においてimplementation-definedという場合、処理系によって動作が異なるかもしれませんが、
その挙動は処理系毎に必ず定義されます。(この結果はその処理系では保証されます)
一方、今回の挙動は”undefined”(いわゆる未定義動作)で、処理系すら原則その結果を保証しません。
言語仕様まで確認してませんでした。ご指摘ありがとうございます。
あとimplementation-definedっていう用語が言語仕様で定義されてることも知りませんでした。タイトルはまあ、だじゃれ的につけたんですが、そういう用語が言語仕様で定義されているとちょっとまずいかもしれないですね(消しました)。
しかし、言語仕様で未定義になっているということは、コンパイラはコンパイル時に警告出すべきじゃないかとも思えますね。少なくともVC2008では警告レベルを最大にしても何も言ってくれませんでしたが、どうなんでしょう。
C/C++言語でいう「未定義」は本当に定義がなく「言語仕様が処理系に何も要求しない」のです。
処理系は、「診断メッセージ(警告)を出す義務もない」ことになっています。
もし言語仕様で「不適格(ill-formed)」と定義されていれば、コンパイルエラーになったことでしょう。
もし診断メッセージが要求されているならば(diagnostic is required)、警告を出したでしょう。
そうでない場合でも、もちろん、親切に警告を出せるなら出しても、動作を文書化して保証してもいいのですが、
言語仕様として未定義である以上は、そこに警告を求めるのは筋違いということになります。
# 言語はプログラマの道具であり、腕の延長であり、安全性はプログラマが最適に考えればよい。
# 道具風情が、プログラマの書きたい処理を阻害しない、言語が負荷を与えない。それがC++の世界だったり。
不適格にするのはそんなに難しくなさそうな気がするんですが、C++はハードボイルドな世界ですね。
そこがいい、って人は多そうですが。
単純なインクリメントなら、チェックも簡単かもしれませんが、
副作用は例えば関数内などでも間接的に発生しますので(参照経由も含め)、
それらを全て検出することはまぁ出来ないかと。
気づいたところだけ指摘するが、全ては指摘できない、
だと、かえって非難されるケースってのもあるのかもしれません。
(気づいたところだけでも教えて欲しいってのもあるかもしれませんが)
遅くなりましたが、本文に反映しました。