1. ホーム
  2. c++

[解決済み】f(i = -1, i = -1)の挙動が未定義なのはなぜ?

2022-04-01 20:29:26

質問

について読んでいました。 評価順の違反 という例を挙げています。

<ブロッククオート

1) スカラーオブジェクトに対する副作用が、同じスカラーオブジェクトに対する別の副作用に対して非シーケンスである場合、その動作は不定となる。

// snip
f(i = -1, i = -1); // undefined behavior

この文脈では i スカラーオブジェクト という意味らしい。

算術型 (3.9.1), 列挙型, ポインタ型, メンバー型へのポインタ (3.9.2), std::nullptr_t, そしてこれらの型の cv-qualified バージョン (3.9.3) はまとめてスカラ型と呼ばれます。

その場合、この文がどう曖昧になるのかがわかりません。第一引数と第二引数のどちらが先に評価されるかに関係なく、そう思えるのです。 i として終了します。 -1 であり、両引数も -1 .

どなたか明確にしていただけませんか?


アップデイト

本当にたくさんの議論に感謝しています。今のところ、私が気に入っているのは HARMICの回答 この文の定義は、一見簡単そうに見えますが、落とし穴や複雑さを露呈しているので、とても参考になりました。 acheong87さん は、参照を使用する場合に生じるいくつかの問題を指摘していますが、それはこの質問の非シーケンス副作用の側面と直交していると思います。


概要

この質問には多くの注目が集まったので、要点と回答をまとめておきます。まず、少し余談ですが、quot;why"には、密接に関連しながらも微妙に異なる意味、すなわちquot;for whatがあることを指摘させてください。 原因 "、"何のために 理由 "、そして"何のために 目的 "。回答は、これらの「なぜ」の意味のうち、どれに対応したものかによってグループ分けします。

何のために

ここでの主な回答は、以下の通りです。 ポール・ドレイパー である。 マーティン・J という質問に対して、「そうですね。ポール・ドレイパーの答えは、次のようなものです。

<ブロッククオート

動作が何であるかが定義されていないので、未定義の動作である。

この回答は、C++の標準が言っていることを説明しているという点で、全体的にとても良いものです。また、以下のような UB の関連するケースも扱っています。 f(++i, ++i);f(i=1, i=-1); . 関連するケースのうち、最初の引数が i+1 と、2番目の i+2 あるいはその逆で、2番目の例では i は関数呼び出しの後に1か-1であるべきです。これらのケースはいずれも以下のルールに該当するため、UBとなります。

スカラー・オブジェクトに対する副作用が、同じスカラー・オブジェクトに対する別の副作用に対して非シーケンスである場合、その動作は不定となります。

したがって f(i=-1, i=-1) は、プログラマの意図が(IMHO)明白で曖昧でないにもかかわらず、同じルールに該当するため、UBでもあるのです。

ポール・ドレイパーも結論で次のように明言している。

定義された行動であったかもしれない?そうです。定義されていたのでしょうか?いいえ。

という疑問が湧きますが、それは「どんな理由・目的で」なのか? f(i=-1, i=-1) 未定義の動作のままにしておくのですか?

何のために

C++の規格には見落としもありますが(不注意かもしれませんが)、多くの見落としは十分な理由があり、特定の目的に沿ったものです。しかし、その目的は、しばしば、「コンパイラを書く人の仕事を楽にする」、あるいは、「より速いコードを書く」のいずれかであることは承知しています。 私は主に、正当な理由があるかどうかを知りたかったのです。 f(i=-1, i=-1) をUBとした。

害悪 スーパーキャット を提供する主な回答です。 理由 をUBで実現しました。Harmic氏は、最適化コンパイラが、一見アトミックな代入演算を複数のマシン命令に分割し、さらにそれらの命令をインターリーブして最適な速度を実現するかもしれないと指摘している。これは、非常に驚くべき結果をもたらすかもしれない。 i は、彼のシナリオでは-2になってしまうのです。このように、harmic は 同じ値 を複数回実行すると、その操作が連続しない場合、悪い影響が出る可能性があります。

を取得しようとしたときの落とし穴について、supercatが関連する解説をしています。 f(i=-1, i=-1) を実行するように見える。彼は、ある種のアーキテクチャでは、同じメモリ・アドレスに同時に複数の書き込みを行うことはできないという厳しい制約があることを指摘している。もし私たちが以下のようなつまらないものを扱っているとしたら、コンパイラはこれを捉えるのに苦労するでしょう。 f(i=-1, i=-1) .

ダビデフ も、harmicのものとよく似たインターリーブ命令の例を示しています。

harmicさん、supercatさん、davidfさんの例は、それぞれやや作為的ではありますが、それらを総合すると、なぜ f(i=-1, i=-1) は未定義の動作であるべきです。

Paul Draperの回答は「何のために」の部分をよりよく扱っていましたが、harmicの回答は「なぜ」のすべての意味を扱うのに最も適していたため、私はharmicの回答を受け入れました。

その他の回答

ジョンB は、オーバーロードされた代入演算子(単なるスカラーではなく)を考慮した場合、同様に問題が発生する可能性があると指摘しています。

どうすればいい?

演算は非シーケンスなので、代入を行う命令をインターリーブできないとは言い切れません。CPUのアーキテクチャによっては、そうするのが最適かもしれません。参照したページにはこのように書かれています。

<ブロッククオート

AがBより前に配列されておらず、BがAより前に配列されていない場合。 の2つの可能性が存在します。

  • AとBの評価は非シーケンス:どのような順序で実行されてもよく、重なってもよい(実行の単一スレッド内では コンパイラはAとBを構成するCPU命令をインターリーブすることができる)

  • AとBの評価は不定順序です:どのような順序で実行されても構いませんが、重なってはいけません。 Bの前にBが完了するか、あるいはAの前にBが完了する。 次に同じ式が評価されるときは、その反対となる。

実行される操作は、値-1をメモリロケーションに格納することだと仮定すれば、それ自体は問題を引き起こさないように思えます。しかし、コンパイラは、同じ効果を持つ別の命令セットに最適化することはできませんが、その命令が同じメモリ位置に対する他の操作とインターリーブされた場合は失敗する可能性があります。

例えば、値-1をロードするよりも、メモリをゼロにしてからデクリメントする方が効率的であったと想像してください。すると、こうなる。

f(i=-1, i=-1)

になるかもしれない。

clear i
clear i
decr i
decr i

これでiは-2。

インチキな例かもしれませんが、可能性はあります。