実際には、なぜ異なるコンパイラがint x = ++ i + ++ i;の異なる値を計算するのでしょうか?


165

このコードを検討してください:

int i = 1;
int x = ++i + ++i;

コンパイラーがこのコードをコンパイルすると仮定して、このコードに対して何を行うかについては、いくつかの推測があります。

  1. 両方とも++i戻り2、結果としてx=4
  2. 1つ++iは戻り2、もう1つはを返し3、結果としてx=5
  3. 両方とも++i戻り3、結果としてx=6

私には、2番目の可能性が最も高いようです。2つの++演算子の1つがで実行されi = 1iがインクリメントされ、結果2が返されます。次に、2番目の++演算子がで実行されi = 2iがインクリメントされ、結果3が返されます。次に2、と3を足し合わせてを与え5ます。

ただし、このコードをVisual Studioで実行したところ、結果はでした6。私はコンパイラをよりよく理解しようとしていますが、何が結果につながる可能性があるのか​​疑問に思ってい6ます。私の唯一の推測は、コードは「組み込み」の並行性で実行できるということです。2つの++演算子が呼び出され、それぞれiがもう一方が戻る前にインクリメントされ、その後、両方ともが返されました3。これは、コールスタックの私の理解と矛盾するため、説明する必要があります。

結果またはC++結果につながるコンパイラーが実行できる(合理的な)ことは何ですか?46

注意

この例は、Bjarne Stroustrupのプログラミング:C ++(C ++ 14)を使用した原則と実践における未定義の動作の例として登場しました。

シナモンのコメントを参照してください。


5
C仕様は、実際には、=の右側での操作または評価の順序をカバーしておらず、インクリメント前/インクリメント後の操作と比較して、左側でのみカバーしています。
クリストボルポリクロノポリス

2
Stroustrupの本からこの例を入手した場合は、質問に引用することをお勧めします(回答の1つへのコメントに記載されています)。
ダニエルR.コリンズ

4
@philipxy提案された複製は、この質問の複製ではありません。質問は異なります。提案された複製の回答は、この質問には回答しません。提案された重複の回答は、この質問に対する承認された(または高投票の)回答の重複ではありません。あなたは私の質問を読み間違えたと思います。私はあなたがそれを読み直して、投票を閉じることを再考することを提案します。
シナモン

3
@philipxy「答えはコンパイラが何でもできると言っています...」それは私の質問に答えません。「彼らは、あなたの質問が異なっていると思っていても、それはその質問の単なるバリエーションであることを示しています」何ですか?「C ++のバージョンを提供していませんが」私のバージョンのC ++は私の質問とは無関係です。「したがって、ステートメントが含まれているプログラム全体で何でもできます」私は知っていますが、私の質問は特定の動作についてでした。「あなたのコメントはそこにある答えの内容を反映していません。」私のコメントは私の質問の内容を反映していますので、もう一度お読みください。
シナモン

2
タイトルに答えるには; UBは、bahavioirが未定義であることを意味するためです。歴史を通してさまざまな時期に、さまざまなアーキテクチャのさまざまな人々によって作成された複数のコンパイラは、線の外側に色を付けて実際の実装を行うように求められたときに、仕様の外側のこの部分に何かを配置する必要があったので、人々はまさにそれを行いましたそれらの中で異なるクレヨンを使用しました。したがって、聖なる格言は、UBに依存しないでください
Toby

回答:


200

コンパイラーはコードを受け取り、それを非常に単純な命令に分割してから、最適と思われる方法でそれらを再結合して配置します。

コード

int i = 1;
int x = ++i + ++i;

次の手順で構成されています。

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

しかし、これは私が書いたように番号付きのリストであるにもかかわらず、ここにはいくつかの順序の依存関係しかありません:1-> 2-> 3-> 4-> 5-> 10-> 11および1-> 6-> 7- > 8-> 9-> 10-> 11は相対的な順序を維持する必要があります。それ以外は、コンパイラは自由に並べ替えることができ、おそらく冗長性を排除できます。

たとえば、次のようにリストを注文できます。

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
4. store tmp1 in i
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

なぜコンパイラはこれを行うことができますか?増分の副作用に対する順序付けがないためです。しかし、コンパイラーは単純化できるようになりました。たとえば、4にはデッドストアがあり、値はすぐに上書きされます。また、tmp2とtmp4は実際には同じものです。

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

そして今、tmp1に関係するすべてはデッドコードです:それは決して使われません。そして、iの再読も排除することができます:

1. store 1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
10. add tmp3 and tmp3, as tmp5
11. store tmp5 in x

ほら、このコードはずっと短いです。オプティマイザーは満足しています。私は一度だけインクリメントされたので、プログラマーはそうではありません。おっと。

代わりにコンパイラーが実行できる他のことを見てみましょう。元のバージョンに戻りましょう。

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

コンパイラは次のように並べ替えることができます。

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

次に、iが2回読み取られていることに再度注意して、そのうちの1つを削除します。

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

それは素晴らしいことですが、さらに進むことができます。tmp1を再利用できます。

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

次に、6でiの再読み取りを排除できます。

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

今4は死んだ店です:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

これで、3と7を1つの命令にマージできます。

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

最後の一時的なものを排除します。

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
10. add tmp1 and tmp1, as tmp5
11. store tmp5 in x

そして今、あなたはVisual C ++があなたに与えている結果を得る。

どちらの最適化パスでも、何もしないために命令が削除されない限り、重要な順序の依存関係が保持されていることに注意してください。


36
現在、これはシーケンスに言及している唯一の答えです。
PM2Ring20年

3
-1この答えが明確になっているとは思いません。観察された結果は、コンパイラの最適化にまったく依存していません(私の答えを参照してください)。
ダニエルR.コリンズ

3
これは、読み取り-変更-書き込み操作を前提としています。ユビキタスx86などの一部のCPUにはアトミックインクリメント操作があり、状況がさらに複雑になります。
マーク

6
@philipxy「標準はオブジェクトコードについて何も言うことはありません。」標準では、このスニペットの動作についても何も言うことはありません-それはUBです。それが質問の前提です。OPは、実際には、コンパイラーが異なる奇妙な結果に到達する可能性がある理由を知りたがっていました。また、私の答えはオブジェクトコードについても何も述べていません。
SebastianRedl20年

5
@philipxyあなたの反対意見がわかりません。前述のように、問題はC ++標準ではなく、UBの存在下でコンパイラーが何をする可能性があるかについてです。架空のコンパイラがコードをどのように変換しているかを調べるときに、オブジェクトコードの使用が不適切なのはなぜですか?実際、オブジェクトコード以外はどのように関連するのでしょうか。
KonradRudolph20年

58

これは(OPが意味するように)UBですが、コンパイラーが3つの結果を取得できる仮想的な方法は次のとおりです。1つの同じ変数ではなく、x異なるint i = 1, j = 1;変数を使用すると、3つすべてで同じ正しい結果が得られますi

  1. 両方の++ iは2を返し、結果としてx = 4になります。
int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4
  1. 1つの++ iは2を返し、もう1つは3を返し、x = 5になります。
int i = 1;
int i1 = ++i;           // i1 = 2
int i2 = ++i;           // i2 = 3
int x = i1 + i2;        // x = 5
  1. 両方の++ iは3を返し、結果としてx = 6になります。
int i = 1;
int &i1 = i, &i2 = i;
++i1;                   // i = 2
++i2;                   // i = 3
int x = i1 + i2;        // x = 6

2
これは私が望んでいたよりも良い反応です、ありがとう。
シナモン

1
オプション1の場合、コンパイラーは事前インクリメントをメモしている可能性がありますi。それが一度だけ起こることができることを知っている、それは一度だけそれを放出します。オプション2の場合、大学のコンパイラクラスプロジェクトが行うように、コードは文字通りマシンコードに変換されます。オプション3の場合、オプション1と似ていますが、プリインクリメントのコピーを2つ作成しました。セットではなく、ベクトルを使用している必要があります。:-)
ZanLynx20年

@dxiv申し訳ありませんが、私の悪いが、私はポスト混ざっ
muru

22

私には、2番目の可能性が最も高いようです。

私はオプション#4に行きます:両方が++i同時に起こります。

新しいプロセッサは、いくつかの興味深い最適化と並列コード評価に移行します。ここで許可されている場合、コンパイラがより高速なコードを作成し続けるもう1つの方法です。私は実用的な実装として、コンパイラーは並列処理に向かっていると考えています。

同じメモリ競合が原因で非決定論的な動作やバス障害を引き起こす競合状態をすぐに確認できました。コーダーがC ++コントラクトに違反したため、すべて許可されていたため、UBです。

私の質問は、C ++コンパイラが4の結果、または6の結果につながる可能性のある(合理的な)ことは何ですか?

それはできたが、それにはカウントされません。

++i + ++i賢明な結果を使用したり、期待したりしないでください。


私がこの答えと@dxivの両方を受け入れることができれば私はそうするでしょう。ご回答ありがとうございます。
シナモン

4
@UriRaz:コンパイラーの選択によっては、プロセッサーがデータの危険性に気付かない場合もあります。たとえば、コンパイラがi2つのレジスタに割り当て、両方のレジスタをインクリメントして、両方を書き戻す場合があります。プロセッサにはそれを解決する方法がありません。基本的な問題は、C ++も最新のCPUも厳密にシーケンシャルではないということです。C ++には、デフォルトで同時に発生することを許可するために、シーケンスの発生前と発生後に明示的にあります。
MSalters

1
ただし、VisualStudioを使用するOPの場合はそうではありません。x86やARMを含むほとんどの主流のISAは、1つのマシン命令が次の命令が開始する前に完全に終了する、完全に順次実行されるモデルの観点から定義されています。スーパースカラーの故障は、単一のスレッドに対してその錯覚を維持する必要があります。(共有メモリを読み取る他のスレッドは、プログラムの順序で物事を見ることが保証されていませんが、OoO execの基本的なルールは、シングルスレッドの実行を中断しないことです。)
PeterCordes20年

1
これは私のお気に入りの答えです。CPUレベルでの並列命令の実行について言及しているのはこれだけだからです。ところで、競合状態が原因で、CPUスレッドが同じメモリ位置でミューテックスのロック解除を待機して停止するため、同時実行モデルではこれは非常に最適ではないことを回答で言及するとよいでしょう。2番目-同じ競合状態のため、実際の答えは4または5、-CPUスレッドの実行モデル/速度に応じて、これは本質的にUBです。
AgniusVasiliauskas20年

1
@AgniusVasiliauskasおそらく、それでも「実際には、なぜ異なるコンパイラが異なる値を計算するのか」は、今日のプロセッサの単純な見方を考えると、より理解しやすいものを探しています。それでも、コンパイラ/プロセッサのシナリオの範囲は、言及されているいくつかのさまざまな回答よりもはるかに大きい場合です。あなたの有用な洞察はさらに別のものです。IMO、並列処理は未来であるため、この回答は、抽象的な方法ではありますが、それらに焦点を当てています。未来はまだ展開中です。IAC、投稿は人気があり、答えを把握しやすいことが最も報われています。
chux -復活モニカ

17

単純で直接的な解釈(コンパイラの最適化やマルチスレッド化に入札することなく)は次のようになると思います。

  1. インクリメント i
  2. インクリメント i
  3. i+を追加i

i二回インクリメントし、その値は3であり、そして一緒に添加した場合、合計は6です。

検査のために、これをC ++関数と見なしてください。

int dblInc ()
{
    int i = 1;
    int x = ++i + ++i;
    return x;   
}

これが、古いバージョンのGNU C ++コンパイラ(win32、gccバージョン3.4.2(mingw-special))を使用して、その関数をコンパイルして得たアセンブリコードです。ここでは、派手な最適化やマルチスレッド化は行われていません。

__Z6dblIncv:
    push    ebp
    mov ebp, esp
    sub esp, 8
    mov DWORD PTR [ebp-4], 1
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    mov eax, DWORD PTR [ebp-4]
    add eax, DWORD PTR [ebp-4]
    mov DWORD PTR [ebp-8], eax
    mov eax, DWORD PTR [ebp-8]
    leave
    ret

ローカル変数iは、スタックの1つの場所(アドレス)にあることに注意してください[ebp-4]。その場所は2回インクリメントされます(アセンブリ関数の5行目から8行目で、そのアドレスの明らかに冗長なロードを含むeax)。次に、9行目から10行目で、その値がに読み込まれeax、に追加されますeax(つまり、現在の値が計算されi + iます)。次に、スタックに冗長的にコピーさeaxれ、戻り値として戻されます(明らかに6になります)。

式のセクション5.4について述べているC ++標準(ここでは古いもの:ISO / IEC 14882:1998(E))を見ると興味深いかもしれません。

特に記載のない限り、個々の演算子のオペランドと個々の式の部分式の評価の順序、および副作用が発生する順序は指定されていません。

脚注付き:

演算子の優先順位は直接指定されていませんが、構文から導出できます。

その時点で、不特定の動作の2つの例が示されています。どちらもインクリメント演算子を使用しています(そのうちの1つは:) i = ++i + 1

ここで、必要に応じて、次のことができます。整数ラッパークラス(Java Integerなど)を作成します。関数operator+をオーバーロードしoperator++、中間値オブジェクトを返すようにします。そして、それを記述して++iObj + ++iObj、5を保持するオブジェクトを返すようにします(簡潔にするために、ここでは完全なコードを含めていません)。

個人的には、上記のシーケンス以外の方法でジョブを実行した有名なコンパイラの例があるかどうか興味をそそられました。最も簡単な実装はinc、加算操作を実行する前に、プリミティブ型に対して2つのアセンブリコードを実行することだと私には思えます。


2
インクリメント演算子には、実際には非常に明確に定義された「戻り」値があります
edc65 2010年

@philipxy:あなたが反対した箇所を取り除くために答えを編集しました。この時点で、あなたは答えにもっと同意するかもしれないし、同意しないかもしれません。
ダニエルR.コリンズ

2
これらは「未定義の振る舞いの2つの例」ではなく、標準の異なる箇所から生じる非常に異なる獣である未定義の振る舞いの2つの例です。脚注の例のテキストでは、C ++ 98が「不特定」と言っていたのを見て、規範的なテキストと矛盾していますが、それは後で修正されました。
Cubbi

@Cubbi:ここで引用されている標準のテキストと脚注はどちらも「未指定」、「直接指定されていない」というフレーズを使用しており、定義セクション1.3.13の用語と一致しているようです。
ダニエルR.コリンズ

1
@philipxy:ここにある多くの回答に対して同じコメントを繰り返しているようです。あなたの主な批評は、OPの質問自体に関するもののようです。その範囲は、抽象的な標準だけではありません。
ダニエルR.コリンズ

7

コンパイラーが実行できる合理的なことは、共通部分式除去です。これは、コンパイラですでに一般的な最適化(x+1)です。より大きな式でlikeのような部分式が複数回発生する場合、計算する必要があるのは1回だけです。たとえばa/(x+1) + b*(x+1)x+1部分式では1回計算できます。

もちろん、コンパイラーは、どの部分式をそのように最適化できるかを知る必要があります。rand()2回呼び出すと、2つの乱数が得られます。したがって、インライン化されていない関数呼び出しはCSEから除外する必要があります。お気づきのように、2回の発生をi++どのように処理するかを示す規則はないため、CSEからそれらを免除する理由はありません。

結果は確かににint x = ++i + ++i;最適化されている可能性がありますint __cse = i++; int x = __cse << 1。(CSE、その後の繰り返しの強度低下)


この規格は、オブジェクトコードについては何も言っていません。これは、言語定義によって正当化されたり、関連したりするものではありません。
philipxy

1
@philipxy:この標準は、未定義の振る舞いの形式については何も言っていません。それが質問の前提です。
MSalters

7

実際には、未定義の動作を呼び出しています。「合理的」と考えることだけでなく、何でも起こり得ます。また、多くの場合、合理的とは思わないこと起こります。すべては定義上「合理的」です。

非常に合理的なコンパイルは、ステートメントを実行すると未定義の動作が呼び出されることをコンパイラが監視するため、ステートメントを実行できないため、アプリケーションを意図的にクラッシュさせる命令に変換されることです。それは非常に合理的です。

反対票:GCCはあなたに強く反対します。


規格が何かを「未定義の振る舞い」として特徴づける場合、それはその振る舞いが規格の管轄外であることを意味します。規格は、その管轄外のものの合理性を判断しようとはせず、適合した実装が不当に役に立たない可能性があるすべての方法を禁止しようとしないため、特定の状況で要件を課さないという規格の失敗は、すべての判断を意味するものではありません。可能な行動も同様に「合理的」です。
スーパーキャット

6

コンパイラが6の結果を得るためにできる合理的なことはありませんが、それは可能で正当なことです。4の結果は完全に妥当であり、5の境界線の結果は妥当であると思います。それらはすべて完全に合法です。

ねえ、ちょっと待って!何が起こらなければならないのか明確ではありませんか?加算には2つの増分の結果が必要なので、明らかにこれらが最初に発生する必要があります。そして、私たちは左から右に行くので...ああ!とても簡単だったら。残念ながら、そうではありません。私たちは左から右に行きませ、そしてそれが問題です。

メモリ位置を2つのレジスタに読み取る(または両方を同じリテラルから初期化し、メモリへのラウンドトリップを最適化する)ことは、コンパイラにとって非常に合理的なことです。これは事実上、それぞれ値が2の2つの異なる変数があり、最終的に4の結果に追加されるという効果があります。これは高速で効率的であり、両方に準拠しているため、「合理的」です標準とコード付き。

同様に、メモリ位置を1回(またはリテラルから初期化された変数)読み取り、1回インクリメントし、その後、別のレジスタのシャドウコピーをインクリメントすると、2と3が加算されます。これは、完全に合法ですが、境界線は合理的だと思います。どちらか一方ではないので、境界線は妥当だと思います。それは「合理的な」最適化された方法でも、「合理的な」正確な衒学的な方法でもありません。やや真ん中です。

メモリ位置を2回インクリメントして(結果は3になります)、その値をそれ自体に追加して最終結果を6にすることは正当ですが、メモリのラウンドトリップを行うことは正確に効率的ではないため、あまり合理的ではありません。優れたストア転送を備えたプロセッサでは、ストアはほとんど見えないはずなので、それを行うのは「合理的」かもしれません...
コンパイラは同じ場所であることを「知っている」ので、インクリメントすることを選択することもできますレジスタ内で値を2回取得してから、それ自体にも追加します。どちらのアプローチでも、6の結果が得られます。

コンパイラは、標準の言い回しによって、そのような結果を与えることを許可されていますが、私は個人的に、不快な部門からの6つの「ファックユー」メモをかなり予想外のことであると考えます(合法かどうか、常に最小限の驚きを与えるようにすることは良いことです!)。しかし、未定義の振る舞いがどのように関係しているかを見ると、悲しいことに、「予期しない」ことについて実際に議論することはできません。

それで、実際には、コンパイラにとって、そこにあるコードは何ですか?clangに聞いてみましょう。これは、うまく尋ねれば(を呼び出して-ast-dump -fsyntax-only)表示されます。

ast.cpp:4:9: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
int x = ++i + ++i;
        ^     ~~
(some lines omitted)
`-CompoundStmt 0x2b3e628 <line:2:1, line:5:1>
  |-DeclStmt 0x2b3e4b8 <line:3:1, col:10>
  | `-VarDecl 0x2b3e430 <col:1, col:9> col:5 used i 'int' cinit
  |   `-IntegerLiteral 0x2b3e498 <col:9> 'int' 1
  `-DeclStmt 0x2b3e610 <line:4:1, col:18>
    `-VarDecl 0x2b3e4e8 <col:1, col:17> col:5 x 'int' cinit
      `-BinaryOperator 0x2b3e5f0 <col:9, col:17> 'int' '+'
        |-ImplicitCastExpr 0x2b3e5c0 <col:9, col:11> 'int' <LValueToRValue>
        | `-UnaryOperator 0x2b3e570 <col:9, col:11> 'int' lvalue prefix '++'
        |   `-DeclRefExpr 0x2b3e550 <col:11> 'int' lvalue Var 0x2b3e430 'i' 'int'
        `-ImplicitCastExpr 0x2b3e5d8 <col:15, col:17> 'int' <LValueToRValue>
          `-UnaryOperator 0x2b3e5a8 <col:15, col:17> 'int' lvalue prefix '++'
            `-DeclRefExpr 0x2b3e588 <col:17> 'int' lvalue Var 0x2b3e430 'i' 'int'

ご覧のとおり、同じものに2つの場所にlvalue Var 0x2b3e430プレフィックスが++適用されており、これら2つはツリー内の同じノードの下にあります。これは、シーケンスなどについて特別なことは何も言われていない非常に非特別な演算子(+)です。何でこれが大切ですか?さて、読んでください。

警告に注意してください:「 'i'への複数のシーケンスされていない変更」。ああ、それはよく聞こえません。どういう意味ですか?[basic.exec]は、副作用と順序付けについて説明し、デフォルトでは、特に明記されていない限り、個々の演算子のオペランドと個々の式の部分式の評価は順序付けされていないことを示しています(段落10)。まあ、くそー、operator+それはそうです-他に何も言われていないので...

しかし、シーケンス前、不確定シーケンス、またはシーケンスなしを気にしますか?とにかく、誰が知りたいですか?

その同じ段落は、順序付けられていない評価が重複する可能性があり、それらが同じメモリ位置を参照している場合(その場合です!)、1つが潜在的に同時ではない場合、動作は未定義であることも示しています。これは本当に醜いところです。なぜなら、それはあなたが何も知らないことを意味し、あなたは「合理的」であるという保証がまったくないからです。不合理なことは、実際には完全に許容され、「合理的」です。


「合理的」の使用は、「コンパイラは何でもできる、単一の命令「xを7に設定」を発行することさえできる」と誰かに言わせないようにするためだけのものでした。
シナモン

@cinnamon何年も前、私が若くて経験の浅いとき、Sunのコンパイラエンジニアは、彼らのコンパイラは、当時私が不合理だと思った未定義の振る舞いのためのコードを生成するために絶対に合理的に行動したと私に言いました。学んだ教訓。
gnasher7 2920

この規格は、オブジェクトコードについては何も言っていません。これは断片化されており、提案された実装が言語定義によってどのように正当化されるか、または関連するかについて不明確です。
philipxy

@philipxy:標準は、整形式で明確に定義されているものとそうでないものを定義します。このQの場合、動作を未定義として定義します。合法であるだけでなく、コンパイラが効率的なコードを生成するという合理的な仮定もあります。はい、あなたは正しいです、標準はそうであることを要求していません。それにもかかわらず、それは合理的な仮定です。
デイモン

@Damon:標準では、すべての実装が定義済みとして処理する必要があるアクションと、実装が定義済みとして処理する必要がないアクションを指定しています。一部のタスクは他のタスクよりも広い範囲のセマンティクスを必要とするため、標準が一部のアクションの動作を定義できなくても、それを定義する実装が、そうでないタスクよりも一部のタスクに適していないことを意味するわけではありません。また、そのような動作を定義しなくても、実装が定義する他の目的よりも、一部の目的に適さなくなることはありません。
supercat

1

ルールがあります:

前のシーケンスポイントと次のシーケンスポイントの間で、スカラーオブジェクトは、式の評価によって、格納されている値を最大で1回変更する必要があります。変更しない場合、動作は定義されません。

したがって、x = 100でも有効な結果になる可能性があります。

私にとって、この例で最も論理的な結果は6です。これは、iの値を2回増やして、それ自体に追加するためです。「+」の両側からの計算値の前に加算を行うことは困難です。

ただし、コンパイラ開発者は他のロジックを実装できます。


0

++ iは左辺値を返すように見えますが、i ++は右辺値を返します。
したがって、このコードは問題ありません。

int i = 1;
++i = 10;
cout << i << endl;

これはそうではありません:

int i = 1;
i++ = 10;
cout << i << endl;

上記の2つのステートメントは、VisualC ++、GCC7.1.1、CLang、およびEmbarcaderoと一致しています。
そのため、VisualC ++およびGCC7.1.1のコードは次のコードと似ています。

int i = 1;
... do something there for instance: ++i; ++i; ...
int x = i + i;

逆アセンブルを見ると、最初にiをインクリメントし、iを書き換えます。追加しようとすると、同じことを行い、iをインクリメントして書き直します。次に、iをiに追加します。 CLangとEmbarcaderoの動作が異なることに気づきました。したがって、最初のステートメントとは一致しません。最初の++ iの後、結果を右辺値に格納してから、2番目のi ++に追加します。
ここに画像の説明を入力してください


「左辺値のように見える」の問題は、コンパイラではなく、C ++標準の観点から話していることです。
MSalters

@MSaltersこのステートメントは、VisualStudio 2019、GCC7.1.1、clang、Embarcadero、および最初のコードと一致しています。したがって、仕様は一貫しています。ただし、2番目のコードでは動作が異なります。2番目のコードは、VisualStudio 2019およびGCC7.1.1と一致していますが、clangおよびEmbarcaderoとは一致していません。
armagedescu

3
ええと、あなたの答えの最初のコードは合法的なC ++なので、明らかに実装は仕様と一致しています。質問と比較すると、「何かをする」はセミコロンで終わり、完全なステートメントになります。これにより、C ++標準で必要とされるが、質問には存在しないシーケンスが作成されます。
MSalters

@MSalters同等の擬似コードとして実行したかったのです。しかし、私はそれを再公式化する方法を確認していない
armagedescu

0

私は個人的に、コンパイラがあなたの例で6を出力することを期待していなかったでしょう。あなたの質問に対する良い詳細な答えはすでにあります。短いバージョンを試してみます。

基本的に、++iこれはこのコンテキストでは2段階のプロセスです。

  1. の値をインクリメントします i
  2. の値を読む i

++i + ++i加算の両側のコンテキストでは、標準に従って任意の順序で評価できます。これは、2つの増分が独立していると見なされることを意味します。また、2つの用語の間に依存関係はありません。したがって、のインクリメントと読み取りはiインターリーブされる可能性があります。これは潜在的な順序を与えます:

  1. i左オペランドのインクリメント
  2. i右オペランドのインクリメント
  3. i左のオペランドを読み戻す
  4. i正しいオペランドを読み戻す
  5. 2つを合計すると:6が得られます

さて、これについて考えると、標準によれば6が最も理にかなっています。結果が4の場合、最初にi独立して読み取り、次に値をインクリメントして同じ場所に書き戻すCPUが必要です。基本的に競合状態です。値が5の場合、一時を導入するコンパイラが必要です。

しかし、標準では++i、変数を返す前、つまり現在のコード行を実際に実行する前に、変数をインクリメントするとしています。合計演算子+i + i、増分を適用した後に合計する必要があります。C ++は、値のセマンティックではなく、変数を処理する必要があると言えます。したがって、私にとって6は、CPUの実行モデルではなく、言語のセマンティクスに依存しているため、今では最も理にかなっています。


0
#include <stdio.h>


void a1(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = x + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void b2(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = i + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void main(void)
{
    a1();
    // b2();
}

stackoverflowへようこそ!回答に制限、仮定、または簡略化を提供できますか。このリンクで回答する方法の詳細を参照してください:stackoverflow.com/help/how-to-answer
UsamaAbdulrehman20年

0

コンパイラの設計に依存するため、答えはコンパイラがステートメントをデコードする方法に依存します。代わりに2つの異なる変数++ xと++ yを使用してロジックを作成することをお勧めします。注:出力は、ms Visual Studioの最新バージョンの言語が更新されているかどうかによって異なります。したがって、ルールが変更されている場合は、出力も変更されます。


0

これを試して

int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4

0

このリンクの評価順序から:

関数呼び出し式の関数引数の評価の順序を含む、C演算子のオペランドの評価の順序、および式内の部分式の評価の順序は指定されていません(以下に記載されている場合を除く)。コンパイラはそれらを任意の順序で評価し、同じ式が再度評価されるときに別の順序を選択する場合があります

引用から、評価の順序がC標準によって指定されていないことは明らかです。コンパイラが異なれば、評価の順序も異なります。コンパイラーは、このような式を任意の順序で自由に評価できます。そのため、コンパイラが異なれば、質問で言及されている式の出力も異なります。

ただし、部分式Exp1とExp2の間にシーケンスポイントが存在する場合は、値の計算とExp1の副作用の両方がシーケンスされ、すべての値の計算とExp2の副作用の前に行われます。


この引用とあなたの声明は、質問に答えることと何の関係がありますか?(レトリック。)とにかく、ここで他の場所で説明されているように、与えられたコードは未定義の振る舞いをしているので、あなたのポイントは当てはまりません。また、質問者は、コードが未定義であるとすると、実装のどの側面がそのようなことをもたらすのかについて(不十分に)尋ねようとしています。また、彼らは、彼らの質問を明確にしない限り、コンパイラが行うのに「合理的」なことは何でも受け入れません。また、この投稿はすでにここにある投稿に何も追加しません。
philipxy

あなたのコメントは私の問題のどれにも対処していません。コードが未定義であり、未指定ではないことを含みます。
philipxy

引用符は未定義について言及しておらず、不特定について言及しています。私はこれで終わりです。
philipxy

はい、それは特定されていません。そのため、コンパイラが異なれば、評価の順序も異なります。そのため、コンパイラごとに異なる出力が得られます。
Krishna KanthYenumula20年


-4

実際には、未定義の振る舞いを呼び出しています。「合理的」と考えることだけでなく、何でも起こり得ます。また、多くの場合、合理的とは思わないこと起こります。すべては定義上「合理的」です。

非常に合理的なコンパイルは、ステートメントを実行すると未定義の動作が呼び出されることをコンパイラが監視するため、ステートメントを実行できないため、アプリケーションを意図的にクラッシュさせる命令に変換されることです。それは非常に合理的です。結局のところ、コンパイラはこのクラッシュが決して起こらないことを知っています。


1
あなたはその質問を誤解していると思います。問題は、特定の結果(x = 4、5、または6の結果)につながる可能性のある一般的または特定の動作に関するものです。「合理的」という言葉の使用が気に入らない場合は、上記のコメントを参照してください。「「合理的」の使用は、コンパイラが何でもできると誰かが言うのを先取りするためだけのものでした。単一の命令を発行することさえできます '"xを7に設定します。"' "一般的な考えを保持する質問についてより良い言い回しがあれば、私はそれを受け入れます。また、回答を再投稿したようです。
シナモン

2
彼らは両方とも非常に類似しているため、あなたの2つの答えの1を削除示唆
MM
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.