f(i = -1, i = -1) が未定義の動作なのはなぜですか? 質問する

f(i = -1, i = -1) が未定義の動作なのはなぜですか? 質問する

評価順序違反について読んでいたのですが、困惑するような例が示されていました。

1) スカラー オブジェクトに対する副作用が、同じスカラー オブジェクトに対する別の副作用に対して順序付けられていない場合、動作は未定義になります。

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

この文脈では、はスカラーオブジェクトiであり、これは明らかに

算術型 (3.9.1)、列挙型、ポインタ型、メンバー型へのポインタ (3.9.2)、std::nullptr_t、およびこれらの型の cv 修飾バージョン (3.9.3) は、総称してスカラー型と呼ばれます。

iこの場合、ステートメントがあいまいになる理由がわかりません。最初の引数と 2 番目の引数のどちらが先に評価されるかに関係なく、は になり-1、両方の引数も になると思われます-1

誰か説明してもらえますか?


アップデート

議論していただき本当に感謝しています。今のところ、@harmic さんの回答は、一見すると非常に簡単に見えるにもかかわらず、このステートメントを定義する際の落とし穴や複雑さを明らかにしているので、とても気に入っています。@acheong87 さんは、参照を使用するときに発生するいくつかの問題を指摘していますが、それはこの質問の順序付けされていない副作用の側面とは無関係だと思います。


まとめ

この質問は多くの注目を集めたので、要点と回答をまとめます。まず、少し脱線しますが、「なぜ」には密接に関連しながらも微妙に異なる意味、つまり「何のために、「何の理由で」、「何の目的で」があるということを指摘しておきます。回答は、「なぜ」のどの意味について言及しているかによって分類します。

何のために

ここでの主な答えはポール・ドレイパーによるもので、マーティン・Jも同様だがそれほど詳しくない答えを寄せている。ポール・ドレイパーの答えは、

動作が何であるかが定義されていないため、未定義の動作となります。

The answer is overall very good in terms of explaining what the C++ standard says. It also addresses some related cases of UB such as f(++i, ++i); and f(i=1, i=-1);. In the first of the related cases, it's not clear if the first argument should be i+1 and the second i+2 or vice versa; in the second, it's not clear if i should be 1 or -1 after the function call. Both of these cases are UB because they fall under the following rule:

If a side effect on a scalar object is unsequenced relative to another side effect on the same scalar object, the behavior is undefined.

Therefore, f(i=-1, i=-1) is also UB since it falls under the same rule, despite that the intention of the programmer is (IMHO) obvious and unambiguous.

Paul Draper also makes it explicit in his conclusion that

Could it have been defined behavior? Yes. Was it defined? No.

which brings us to the question of "for what reason/purpose was f(i=-1, i=-1) left as undefined behavior?"

for what reason / purpose

Although there are some oversights (maybe careless) in the C++ standard, many omissions are well-reasoned and serve a specific purpose. Although I am aware that the purpose is often either "make the compiler-writer's job easier", or "faster code", I was mainly interested to know if there is a good reason leave f(i=-1, i=-1) as UB.

harmic and supercat provide the main answers that provide a reason for the UB. Harmic points out that an optimizing compiler that might break up the ostensibly atomic assignment operations into multiple machine instructions, and that it might further interleave those instructions for optimal speed. This could lead to some very surprising results: i ends up as -2 in his scenario! Thus, harmic demonstrates how assigning the same value to a variable more than once can have ill effects if the operations are unsequenced.

supercat provides a related exposition of the pitfalls of trying to get f(i=-1, i=-1) to do what it looks like it ought to do. He points out that on some architectures, there are hard restrictions against multiple simultaneous writes to the same memory address. A compiler could have a hard time catching this if we were dealing with something less trivial than f(i=-1, i=-1).

davidf also provides an example of interleaving instructions very similar to harmic's.

Although each of harmic's, supercat's and davidf' examples are somewhat contrived, taken together they still serve to provide a tangible reason why f(i=-1, i=-1) should be undefined behavior.

I accepted harmic's answer because it did the best job of addressing all meanings of why, even though Paul Draper's answer addressed the "for what cause" portion better.

other answers

JohnB points out that if we consider overloaded assignment operators (instead of just plain scalars), then we can run into trouble as well.

ベストアンサー1

Since the operations are unsequenced, there is nothing to say that the instructions performing the assignment cannot be interleaved. It might be optimal to do so, depending on CPU architecture. The referenced page states this:

If A is not sequenced before B and B is not sequenced before A, then two possibilities exist:

  • evaluations of A and B are unsequenced: they may be performed in any order and may overlap (within a single thread of execution, the compiler may interleave the CPU instructions that comprise A and B)

  • evaluations of A and B are indeterminately-sequenced: they may be performed in any order but may not overlap: either A will be complete before B, or B will be complete before A. The order may be the opposite the next time the same expression is evaluated.

That by itself doesn't seem like it would cause a problem - assuming that the operation being performed is storing the value -1 into a memory location. But there is also nothing to say that the compiler cannot optimize that into a separate set of instructions that has the same effect, but which could fail if the operation was interleaved with another operation on the same memory location.

For example, imagine that it was more efficient to zero the memory, then decrement it, compared with loading the value -1 in. Then this:

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

might become:

clear i
clear i
decr i
decr i

Now i is -2.

It is probably a bogus example, but it is possible.

おすすめ記事