Photo by Alan Becker Capuyá
青木です。
先日、paizaのツイッターアカウント(@paiza_official)で出題した四択問題について、皆様からたくさんのご指摘・ご批判をいただいたので、その経緯と結論をお伝えします。
次のような問題を考えて出題しました。
int i = 0;
— paiza[パイザ] (@paiza_official) 2016年12月26日
のときに評価値が1になるのは
【補足】C言語,C++,Javaを想定しています
— paiza[パイザ] (@paiza_official) 2016年12月26日
当初は、それぞれの評価値は順に2, 3, 1, 2となり、3つめの"i++ + i++"の選択肢が答えとなることを想定していました。
ですが、しばらくすると、次のようなリプライやツイートをいただきました。
全部C(たぶんC++/Javaも)だと未定義だと思います
— にゃんにゃん (@Oo_nyannyan_oO) 2016年12月26日
https://t.co/1Ui3vFVQMH なんの言語かは知らないが、Cなら未定義だった気が…
— prioninae (@prioninae) 2016年12月26日
インクリメント演算子(++)で変数iに数字の1が加えられる順序が、言語仕様では未定義なので、答えられない問題のようです……。
未定義だと、動作が言語の実装に依存しますので、コンパイラなどの環境によって、結果が変わってしまいます。
どのように動作するのかの制限がありあません。(実装に依存する、と述べてましたが、実装に依存して特定の動作が定まるかどうかすら定義されません)
急いで調査したところ、ひとまずJavaでは定義されていることが判明しました。
C言語、C++についての詳細はわからないものの、雲行きが怪しそうだったので、訂正ツイートを行いました。
【補足】想定言語はJavaです、失礼いたしました…m(__)m
— paiza[パイザ] (@paiza_official) 2016年12月26日
するとさっそく反響が……。
paizaがこのツイートをする。
— YSR@サノバウィッチ攻略中 (@YSRKEN) 2016年12月26日
paiza「C言語,C++,Javaを想定しています」
↓
マサカリが投げられる
↓
paiza「想定言語はJavaです、失礼いたしました…m(__)m」
↓
江添氏「C++14まで未定義だったが」https://t.co/fFpc4uYBv6 https://t.co/f4spNn7DZs
マサカリ投げられたと言われてしまいました。
そして、C++標準化委員会の方からも
この問題は、C++14までは未定義。C++17では式の評価順序、式の副作用のコミット順序が定義されたので、成り立つ。https://t.co/CQaMurnZcVhttps://t.co/MTLIMKt2wr
— Ryou Ezoe(江添 亮) (@EzoeRyou) 2016年12月26日
C++を想定言語から外していましたが、C++17ではちゃんと決まる……?
途中で問題を変え、ごらんいただいた方を混乱させてしまい、申し訳ございません。
では、この問題が各言語で成立するのかどうなのか、詳しく説明させていただきます。
■Javaの場合
まず、Java言語の仕様を調べてみました。
OracleのThe Java Language Specification, Java SE 8 Editionの"15.7.1. Evaluate Left-Hand Operand First"で、次のような説明を見つけました。
The left-hand operand of a binary operator appears to be fully evaluated before any part of the right-hand operand is evaluated.
binary operatorの左側の式は、右側の式の前に評価されるようです。
演算子の"+"はbinary operatorですので、正確に式を評価できそうです。
この場合、例えば i++ + ++i では
- 左側のi++は、0と評価されます。
- その後、後置インクリメントによって、iの値が1になります。
- そして、右側の++iが評価されます。前置インクリメントは、評価される前に加算されるので、右側の++iは2と評価されます。
- よって、左側のi++は0、右側の++iは2と評価されましたので、評価値は2となります。
最初の出題時の想定通りの評価値となりました。
「Java8では、出題した問題の式は評価できるので今回の問題は成り立ちます」
ただし、先ほどの"15.7.1. Evaluate Left-Hand Operand First"の上には次のような記述があります。
It is recommended that code not rely crucially on this specification.
Code is usually clearer when each expression contains at most one side effect, ...
ということで、この仕様に依存しないようにコードを書くべきだと述べられてます。
side effectが文中にいくつも現れるコードは、紛らわしいことこの上ないですね。
ですので、(たぶんやらないかとは思いますが)仕事のプログラムなどで、こんな記述はしない方がよさそうです。
■C++の場合
同様に、今度はC++の仕様書を見てみましょう。
isocpp.orgで公開されているC++14のdraftを確認してみました。
1300ページ以上あってとても一度にすべてを読めたものではないので、ひとまず"increment"で検索してみました。
すると、1.9.15のExampleに、次のような記述を見つけました。
i = i++ + 1; // the behavior is undefined
i++の後ろに、+で数値をつないだ場合、動作が定義されてないようです。
i++と同時にiへの代入を行う場合の動作が定義されてないようです。
「C++14では、出題した問題の式は評価できないので今回の問題は成り立ちません」
しかし、先ほどのツッコミが。
この問題は、C++14までは未定義。C++17では式の評価順序、式の副作用のコミット順序が定義されたので、成り立つ。https://t.co/CQaMurnZcVhttps://t.co/MTLIMKt2wr
— Ryou Ezoe(江添 亮) (@EzoeRyou) 2016年12月26日
C++では定義されているのですね!急いでC++17の仕様書を確認しに行きました。
isocpp.orgの最新のドラフトを見ます。
1.9.18ののExampleにそれらしき記述を見つけました。
i = i++ + 1; // the value of i is incremented
i = i++ + i; // the behavior is undefined
むむむ、i++ +1は定義されるようになったけど、i++ + iは未定義とあります……。i++ + iが未定義なら、i++ + ++iも未定義な気がします……。
i = i++ +1は定義されるようになったけど、i = i++ + iは未定義とあります……。i = i++ + iが未定義なら、i++ + ++iも未定義な気がします……。
よくわからなくなってきたので、ご指摘いただいた江添さんのブログに凸りました。(質問コメントしているのは私です)
cpplover.blogspot.jp
お返事いただけました。
間違えていたようだ。まだ§1.10 p18に未定義として書かれている。 https://t.co/RcunquRRw8
— Ryou Ezoe(江添 亮) (@EzoeRyou) 2016年12月26日
やっぱり未定義だそうです。
「C++14でもC++17でも、出題した問題の式の評価が未定義なので今回の問題は成り立ちません」
ただ、C++17ではside effectのある式の評価が見直されてるようなので、今後は変わっていくかもしれませんね。
■C言語の場合
C11の仕様書のドラフトを確認します。
6.5.2.4 Postfix increment and decrement operatorsの2に次のような記述がありました。
The side effect of updating the stored value of the operand shall occur between the previous and the next sequence point.
どうやら、"sequence points"の範囲の中でside effectが評価されるようです。
"sequence points"の定義に"+"演算子は含まれません。(sequence pointsについては、同仕様書のAnnex Cに書かれています。Wikipediaのsequence pointsに関する説明の方が具体例が載ってて分かりやすいかもしれません)
ですので、i++ + ++iの式の中で、i++によって、後置インクリメントされるタイミイングが決められておらず、++iを評価する前と後のどちらで、iの値が1増加するのか定めることができません。
iの値が1増加するタイミイングが、++iを評価する前と後のどちらなのかが、ここからだけでは読み取れません。
さらに、Annex JのJ.2 Undefined behaviorのリストに、次のような記述があります。
A side effect on a scalar object is unsequenced relative to either a different side effect on the same scalar object or a value computation using the value of the same scalar object (6.5).
同じオブジェクトに対しての異なるside effectによって順序が決まらない場合は、未定義とのことです。
ということは、「C11では、出題した問題の式の評価が未定義なので今回の問題は成り立ちません」
■まとめ
- Java8では定義されていて、出題した問題は解ける。(ただし推奨されている書き方ではない)
- C++14とC++17では未定義なので、出題した問題は解けない。
- Cでは未定義なので、出題した問題は解けない。
というわけで、ツイッターで訂正させていただきました通り、今回の問題は想定言語はJavaのみとなります。
詳細を確認しないままツイートして、皆様を混乱させてしまいまして申し訳ございません。また、鋭いご指摘をいただいた皆様、ありがとうございました。
paizaは、技術を追い続けることが仕事につながり、スキルのある人がきちんと評価される場を作ることで、日本のITエンジニアの地位向上を目指したいと考えています。
自分のスキルを磨いていきたいと考えている方におすすめなのが「paizaラーニング」。オンラインでプログラミングしながらスキルアップできる入門学習コンテンツです。初心者でも楽しくプログラミングの基本を学ぶことができます。
そして、paizaでは、Webサービス開発企業などで求められるコーディング力や、テストケースを想定する力などが問われるプログラミングスキルチェック問題も提供しています。
スキルチェックに挑戦した人は、その結果によってS・A・B・C・D・Eの6段階のランクを取得できます。必要なスキルランクを取得すれば、書類選考なしで企業の求人に応募することも可能です。「自分のプログラミングスキルを客観的に知りたい」「スキルを使って転職したい」という方は、ぜひチャレンジしてみてください。