
2026/05/12 23:14
int a = 5; a = a++ + ++a; の後、a はいくつになるのか?(2011 年の問題)
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
C/C++ コードで結合された事前増分演算子と事後増分演算子を利用するもの(例:
a = a++ + ++a)は未定義動作 (UB) を招き、コンパイラの実装および最適化に依存して 11 から 14 までの予測不可能な結果を出力します。Java や C# のように言語仕様で左から右の評価を強制するもの(例:JLS セクション 15.7)とは異なり、レガシーおよび現代的な C/C++ コンパイラはオペランドを取得する段階が異なります。例えば、gcc バリエーションは常に [6–14] の範囲の結果を返し、Clang バージョンは [5–13] のような値を生み出し、Microsoft の出力は演算子オーバーロードを有効にすると完全にシフトします。Borland Turbo C++、Keil C、SDCC、HiSoft C といった特定の歴史的コンパイラでも、同じ式に対して異なる、非移植性の結果セットを返します。これらの環境間で単一の標準的な答えが存在しないため、複数システムをターゲットとする開発者は論理エラー(コンパイラ依存の評価順序による)を防ぐためにこれらの構成を完全に回避する必要があります。本文
C/C++ における未定義振る舞い(UB)に関する考察報告書
Furio 氏より提供された「パズル」と題する課題は、数日間にわたり、関心が薄い方も含め広く周知されました。本件の最も興味深い点は、未定義振る舞い(Undefined Behavior: UB) が複合的に存在することです。これにより、結果として 11、12、13 の 3 つの異なる答えが理論上可能となります。
以下にまず理論的な考察を述べ、次に実証的な結果について紹介いたします(初期データは nism0 氏による収集ですが、後にポーランド側のミラーサイトを通じて多数の参加者により拡張されました)。
1. 未定義振る舞い(UB)が生じる箇所
第一の UB:取得順序の不確定性
まず不明なのは、どの
a の値が先にメモリからレジスタへコピーされるかという点です。2 つの解釈可能性が存在します。
可能性 1:最初の
を先に取得するパターンa
// a = a + ++a; // ステップ 1: 最初の a を読み出す (a == 5) // ステップ 2: 前置インクリメントを実行 (a == 6) // ステップ 3: 二番目の a を読み出す (値は 6) // ステップ 4: 加算計算 // 結果:11
可能性 2:前置インクリメントを先に処理し、その結果をメモリに格納してから両方の
を取得するパターンa
// a = a + ++a; // ステップ 1: 前置インクリメントを実行 (a == 6) -> メモリへの書き込み // ステップ 2 & 3: 両方の a を読み出す (値はどちらも 6) // ステップ 4: 加算計算 // 結果:12
このように、単に後置インクリメントを無視した場合においても、11 と 12 という 2 つの異なる結果が生じる可能性があります。
第二の UB:後置インクリメントと代入の競合 (a = a++
)
a = a++a = a++ という一行には、以下の 2 つのパターンが存在します(int a = 5; を例として考察)。
用語の定義:
: メモリ上に保持されたa_mem
の値a
: レジスタなどにコピーされたa_copy
の値a
可能性 1:後置インクリメントの結果が「行方不明」になるパターン
初期状態: (a_mem == 5, a_copy == なし) ステップ 1: a を読み出す -> (a_mem=5, a_copy=5) ステップ 2: メモリ上の後置インクリメントを実行 -> (a_mem=6, a_copy=5) ※ここでコピーされた値は古いまま維持される ステップ 3: 代入を実行 -> a_copy(5) が a_mem に書き戻される -> (a_mem=5, a_copy=無効化) 結果:後置インクリメントの結果が上書きされ、実質的に無効化される。
可能性 2:後置インクリメントを計算の最末端まで遅延させるパターン
初期状態: (a_mem == 5, a_copy == なし) ステップ 1: a を読み出す -> (a_mem=5, a_copy=5) ステップ 2: 代入を実行 -> (a_mem=5, a_copy=5) ※値は変更されない(コピーが元の値を参照) ステップ 3: メモリ上の後置インクリメントを実行 -> (a_mem=6, a_copy=無効化) 結果:計算終了直後に後置インクリメントが適用される。
UB のまとめ (a = a++ + ++a
の場合)
a = a++ + ++a上記の 2 つの UB を組み合わせると、以下の 4 つの結果が理論的に導き出されます。
| 前処理パターン | 後置インクリメント振る舞い | 加算に使用される値 | 結果 |
|---|---|---|---|
| 可能性 1 (先読み) | 可能性 1 (結果失効) | 5 + 6 | 11 |
| 可能性 1 (先読み) | 可能性 2 (最終実行) | 5 + 6 | 12 |
| 可能性 2 (増分後読み) | 可能性 1 (結果失効) | 6 + 6 | 12 |
| 可能性 2 (増分後読み) | 可能性 2 (最終実行) | 6 + 6 | 13 |
2. 実証的結果
以下の表は、nism0 氏による初期テストと、nonek、qyon 氏らによる修正版(タイポ訂正を含む)を示しています。
| コードパターン | Code 1 | Code 2 | Code 3 | Code 4 | Code 5 | Code 6 |
|---|---|---|---|---|---|---|
| gcc 2.95 | 12 | 13 | 14 | 13 | 6 | 12 |
| gcc 4.1 | 12 | 13 | 14 | 13 | 6 | 12 |
| gcc 4.2 | 12 | 13 | 14 | 13 | 6 | 12 |
| gcc 4.2.1 (Apple) | 12 | 13 | 14 | 13 | 6 | 12 |
| gcc 4.3 | 12 | 13 | 14 | 13 | 6 | 12 |
| gcc 4.3.3 | 12 | 13 | 13 | 14 | 6 | 12 |
| gcc 4.4.4 | 12 | 13 | 13 | 14 | 6 | 12 |
| gcc 4.6.0 (exp.) | 12 | 13 | 14 | 13 | 6 | 12 |
| gcc 4.5.1 MinGW64 | 12 | 13 | 13 | 14 | 6 | 12 |
| tcc 0.9.25 | ?? | ?? | ?? | ?? | 5 | 12 |
| bcc 0.16.17 | ?? | ?? | ?? | ?? | 5 | 12 |
| Microsoft C/C++ (80x86) | 12 | 13 | 13 | 14 | 6 | 12 |
| Embarcadero C++ (Win32) | 12 | 13 | 13 | 14 | 6 | 12 |
| Intel C++ | 12 | 12 | 13 | 13 | 6 | 12 |
| Keil C | 2 | 11 | 12 | 12 | 13 | 6 |
| SDCC | #6092 | 11 | 12 | 13 | 14 | 5 |
| clang 2.8 | 11 | 12 | 12 | 13 | 5 | 11 |
| clang 1.6 (Apple) | 11 | 12 | 12 | 13 | 5 | 11 |
| PHP 5.2.10 | 11 | 12 | 12 | 13 | 5 | 12 |
| java 1.6.0_06 | 11 | 12 | 12 | 13 | 5 | 11 |
| javac 1.4.2_12 | 11 | 12 | 12 | 13 | 5 | 11 |
| java 1.6.0_21 | 11 | 12 | 12 | 13 | 5 | 11 |
| javac 1.6.0_22 | 11 | 12 | 12 | 13 | 5 | 11 |
| C# 2.0 | 11 | 12 | 12 | 13 | 5 | 11 |
| C# 4.0 | 11 | 12 | 12 | 13 | 5 | 11 |
| C# Mono 2.6.4 | 11 | 12 | 12 | 13 | 5 | 11 |
| Borland Turbo C++ (DOS) | 12 | 13 | 13 | 14 | 6 | 12 |
| HiSoft C (ZX Spectrum) | 11 | 12 | 12 | 13 | 5 | 12 |
※補足:Garbaty lamer 氏は、ポーランド側のミラーサイトにて HiSoft C for ZX Spectrum のスクリーンショットを投稿されています(クリックすると拡大可能)。
追加のテストコード提供
Icewall、Krzysztof Kotowicz(PHP)、mlen(Clang/GCC)、none'a(Java/Keraj)、MDobak(SDCC/Keil)、garbaty lamer(C# その他)、Xgrzyb90(GCC)、no_name(GCC)、dikamilo(MinGW)等の皆様に、追加の結果をご提供いただきました。
さらに、ポーランド側のミラーサイトにて以下のコード(nism0 氏作成)を試すことができます(第 3 節参照)。
負荷オーバーロードテスト (付録 1)
ポーランド側で Rolek 氏は、オーバーロード演算子にも同様のテストを適用することを提案しました。MSVC++ および g++ による結果は以下の通りです。
| コードパターン | Code 1 | Code 2 | Code 3 | Code 4 | Code 5 | Code 6 |
|---|---|---|---|---|---|---|
| MSVC++ (オーバーロードなし) | 12 | 13 | 13 | 14 | 6 | 12 |
| MSVC++ (オーバーロードあり) | 11 | 13 | 12 | 14 | 5 | 12 |
| g++ 4.5.0 MinGW (オーバーロードなし) | 12 | 13 | 13 | 14 | 6 | 12 |
| g++ 4.5.0 MinGW (オーバーロードあり) | 11 | 13 | 12 | 14 | 5 | 12 |
その他の重要な情報
シーケンスポイントに関する文献
krlm 氏は、シーケンスポイントに関する優れた記事をリンクとして提供されています。
C# での挙動 (付録 2)
Garbaty lamer 氏は、C# ではこのパズルは実際には「難問」ではなく、明確に定義されていると述べています(C# 仕様 §7.3)。
「式内の演算子は左から右の順序で評価されます。例:
の場合、F は i の旧値を、G は i の旧値を、H は i の新値をそれぞれ用います。これはオペランドの優先順位とは無関係です。」F(i) + G(i++) * H(i)
Java での挙動
Cem Paya 氏のコメントによると、Java でも厳密に左から右の評価が行われるため、同様に難問ではありません(Java Language Specification section 15.7)。
コンパイル最適化の影響 (C++ 特有)
C++ では未定義であるとされていますが、コンパイル時の最適化レベルによって挙動が変わることがあります。例えば、コンパイラは「
a の値は評価中に不変である」と想定し、他の参照をその取得した値と同じものとして最適化を行う場合があります。
付録:テスト用ソースコード (付録 3)
#include <stdio.h> int main(void){ int a = 5, b = 5, c = 5, d = 5, e = 5, f = 5; // test 1 a = a++ + a++; printf("%i \n",a); // test 2 b = b++ + ++b; printf("%i \n",b); // test 3 c = ++c + c++; printf("%i \n",c); // test 4 d = ++d + ++d; printf("%i \n",d); // test 5 e = e++; printf("%i \n",e); // test 6 f = f + ++f; printf("%i \n",f); // done return 0; }
付録 2: マスタークラス・バイナリファイルワークショップへの案内
バイナリファイルおよびプロトコルスキルを向上させたい方へ、4 月〜6 月に開催するワークショップ「Mastering Binary Files and Protocols: The Complete Journey」をお勧めいたします。
※注記:本文書内の表において一部の行が欠落している箇所や、原文のフォーマット上の不整合が見受けられる場合(例:
?? の連続など)は、データ取得時の未確認結果またはコンパイラ固有のエラー/挙動を反映したものです。