web-dev-qa-db-ja.com

C ++ 11では、 `i + = ++ i + 1`は未定義の動作を示しますか?

この質問は私が読んでいるときに出てきました(答え) それで、なぜi = ++ i +1がC++ 11で明確に定義されているのですか?

微妙な説明は、(1)式_++i_は左辺値を返しますが、_+_はオペランドとしてprvaluesを受け取るため、左辺値からprvalueへの変換を実行する必要があるということです。これには、その左辺値の現在の値(iの古い値より1つ多い値ではなく)の取得が含まれるため、シーケンスする必要がありますafterインクリメントによる副作用(つまり、iの更新)(2)割り当てのLHSも左辺値であるため、その値の評価には、iの現在の値のフェッチは含まれません。この値の計算はシーケンスされていませんが、w.r.t。 RHSの値の計算では、これは問題を引き起こしません(3)割り当て自体の値の計算には、(再び)iの更新が含まれますが、RHSの値の計算後、つまりiへの事前の更新後にシーケンスされます。問題ない。

結構ですので、UBはありません。ここで私の質問は、割り当て演算子を_=_から_+=_(または同様の演算子)に変更した場合はどうなるかということです。

式_i += ++i + 1_の評価は、未定義の動作につながりますか?

私が見ているように、標準はここでそれ自体と矛盾しているようです。 _+=_のLHSは依然として左辺値であるため(そしてそのRHSは依然としてprvalueである)、(1)と(2)に関する限り、上記と同じ理由が当てはまります。 _+=_のオペランドの評価に未定義の動作はありません。 (3)に関しては、複合代入_+=_の操作(より正確には、その操作の副作用。必要に応じて、その値の計算は、いずれの場合もその副作用の後に順序付けられます)は両方iの現在の値をフェッチし、そして(標準で明示的に指定されていない場合でも、明らかにその後にシーケンスされます。そうでない場合、そのような演算子の評価は常に未定義の動作を呼び出す)RHSを追加し、結果をiに保存します。これらの操作は両方とも、シーケンスされていない場合、未定義の動作を示します。 _++_の副作用ですが、上記で説明したように(_++_の副作用は、_+_の値の計算の前にシーケンスされ、_+=_演算子のRHSが得られます。どの値の計算がその複合割り当ての操作の前に順序付けられるか)、そうではありません。

しかし一方で、標準では、(左辺値)Eが1回だけ評価されることを除いて、_E += F_は_E = E + F_と同等であるとも述べています。この例では、iの値の計算(これはEがここにあるものです)as lvalueには、w.r.tでシーケンスする必要のあるものは何も含まれていません。他のアクションなので、1回または2回実行しても違いはありません。式は_E = E + F_と厳密に同等である必要があります。しかし、ここに問題があります。 i = i + (++i + 1)を評価すると、未定義の動作が発生することは明らかです。何が得られますか?それともこれは標準の欠陥ですか?

追加。副作用と値の計算を適切に区別し、「評価」を使用するために、上記の説明を少し変更しました。両方を包含する式の標準)を行います。私の主な質問は、この例で動作が定義されているかどうかだけではなく、これを決定するために標準をどのように読まなければならないかということだと思います。特に、_E op= F_と_E = E op F_の同等性を、複合代入演算のセマンティクスの最終的な権限として(この場合、例には明らかにUBが含まれています)、または単に数学的なものを示すものと見なす必要があります。演算は、割り当てられる値の決定に関係します(つまり、opで識別される値で、複合代入演算子の左辺値から右辺値に変換されたLHSを左オペランド、RHSを右オペランドとして)。後者のオプションは、私が説明しようとしたように、この例でUBについて議論することをはるかに難しくします。同等性を信頼できるものにしたいと思うことは認めますが(複合割り当てが一種の第2クラスのプリミティブになり、その意味は第1クラスのプリミティブの観点から書き直すことによって与えられます。したがって、言語定義は単純化されます)、これに対してかなり強い議論です:

  • Eは1回だけ評価される」という例外があるため、同等性は絶対的なものではありません。この例外は、Eの評価に副作用の未定義の動作が含まれる場合、たとえば、かなり一般的な_a[i++] += b;_の使用法など、使用を避けるために不可欠であることに注意してください。事実なら、複合割り当てを排除するための完全に同等の書き換えは不可能だと思います。架空の_|||_演算子を使用してシーケンスされていない評価を指定すると、_E op= F;_(簡単にするためにintオペランドを使用)を_{ int& L=E ||| int R=F; L = L + R; }_と同等に定義しようとしますが、例にはUBがありません。いずれにせよ、標準は私たちに書き直しのレシピを与えていません。

  • この標準では、複合割り当てを、セマンティクスの個別の定義が不要な第2クラスのプリミティブとして扱いません。たとえば5.17(強調鉱山)

    代入演算子(=)と複合代入演算子はすべて右から左にグループ化します。 [...]すべての場合で、代入は、右オペランドと左オペランドの値計算の後、代入の値計算の前に順序付けられます。式。不確定に順序付けられた関数呼び出しに関しては、複合代入の操作は単一の評価です。

  • 複合割り当てを単純な割り当ての単なる省略形にすることを意図している場合、この説明に明示的に含める理由はありません。最後のフレーズは、同等性が信頼できると見なされた場合にどうなるかと直接矛盾します。

複合代入が独自のセマンティクスを持っていることを認める場合、それらの評価には(数学演算は別として)単なる副作用(代入)と値の評価(代入の後にシーケンスされる)以上のものが含まれるという点が生じますが、また、LHSの(前の)値をフェッチする名前のない操作。これは通常、「左辺値から右辺値への変換」という見出しの下で処理されますが、LHS 右辺値としてオペランドをとる演算子が存在しないため、ここでこれを行うことを正当化するのは困難です。拡張された「同等の」形式のものがありますが)。 _++_の副作用との潜在的な順序付けられていない関係がUBを引き起こすのはまさにこの名前のない操作ですが、名前のない操作はそうではないため、この順序付けられていない関係は標準で明示的に述べられていません。存在そのものが標準に暗黙的に含まれている操作を使用してUBを正当化することは困難です。

47

表現:

i += ++i + 1

未定義の動作を呼び出します。言語弁護士の方法では、次の結果になる欠陥レポートに戻る必要があります。

i = ++i + 1 ;

c ++ 11で明確に定義されるようになりました。これは 欠陥レポート637。シーケンス規則と例が一致しません 、次のようになります。

1.9 [intro.execution]段落16でも、未定義の動作の例として次の式がリストされています。

i = ++i + 1;

ただし、新しいシーケンス規則により、この式が明確に定義されているようです。

レポートで使用されるロジックは次のとおりです。

  1. 割り当ての副作用は、LHSとRHSの両方の値の計算後にシーケンス処理する必要があります(5.17 [expr.ass]段落1)。

  2. LHS(i)は左辺値であるため、その値の計算にはiのアドレスの計算が含まれます。

  3. RHS(++ i + 1)を値計算するには、最初に左辺値式++ iを値計算してから、結果に対して左辺値から右辺値への変換を行う必要があります。これにより、加算演算の計算の前にインクリメントの副作用がシーケンスされ、加算演算の計算が割り当ての副作用の前にシーケンスされることが保証されます。つまり、この式の明確な順序と最終値が得られます。

したがって、この質問では、問題によってRHSが変更されます。

++i + 1

に:

i + ++i + 1

ドラフトC++ 11標準 セクション5.17代入および複合代入演算子によるもの:

E1 op = E2の形式の式の動作は、E1が1回だけ評価されることを除いて、E1 = E1 opE2と同等です。 [...]

これで、i内のRHSの計算が++iに対して順序付けられていない状況になり、未定義の動作が発生します。これは、セクション1.9段落15から続きます。

特に明記されていない限り、個々の演算子のオペランドおよび個々の式の部分式の評価は順序付けられていません。 [注:プログラムの実行中に複数回評価される式では、その部分式のシーケンスされていない評価と不確定にシーケンスされた評価を、異なる評価で一貫して実行する必要はありません。 —end note]演算子のオペランドの値の計算は、演算子の結果の値の計算の前にシーケンスされます。スカラーオブジェクトの副作用が、同じスカラーオブジェクトの別の副作用、または同じスカラーオブジェクトの値を使用した値の計算に比べて順序付けられていない場合、動作は定義されていません。

これを示す実用的な方法は、clangを使用してコードをテストすることです。これにより、次の警告が生成されます(ライブで参照):

warning: unsequenced modification and access to 'i' [-Wunsequenced]
i += ++i + 1 ;
  ~~ ^

このコードの場合:

int main()
{
    int i = 0 ;

    i += ++i + 1 ;
}

これは、 -Wunsequencedclang'sテストスイートのこの明示的なテスト例によってさらに強化されます。

 a += ++a; 
5
Shafik Yaghmour

はい、UBです!

あなたの表現の評価

_i += ++i + 1
_

次の手順に進みます。

5.17p1(C++ 11)の状態(私の強調):

代入演算子(=)と複合代入演算子はすべて右から左にグループ化します。すべて、左のオペランドとして変更可能な左辺値を必要とし、左のオペランドを参照する左辺値を返します。左側のオペランドがビットフィールドの場合、すべての場合の結果はビットフィールドになります。 すべての場合において、代入は、右オペランドと左オペランドの値計算の後、代入式の値計算の前に順序付けられます。

「値の計算」とはどういう意味ですか?

1.9p12は答えを与えます:

Volatile glvalue(3.10)で指定されたオブジェクトへのアクセス、オブジェクトの変更、ライブラリI/O関数の呼び出し、またはこれらの操作のいずれかを実行する関数の呼び出しはすべて副作用であり、実行環境の状態の変化です。 式(または部分式)の評価には、一般に両方の値の計算が含まれます(glvalue評価用のオブジェクトのIDの決定を含む)そして、prvalue評価のためにオブジェクトに以前に割り当てられた値をフェッチします)そして副作用の開始。

コードは複合代入演算子を使用しているため、5.17p7は、この演算子の動作を示しています。

_E1 op= E2_形式の式の動作は、_E1 = E1 op E2 except that_ E1と同等です。評価されるのは1回だけです。

したがって、式E1 ( == i)の評価には、iで指定されたオブジェクトのIDとlvalue-to-rvalueそのオブジェクトに格納されている値をフェッチするための変換。ただし、2つのオペランド_E1_と_E2_の評価は、相互に順序付けられていません。したがって、E2 ( == ++i + 1)の評価が副作用(iの更新)を開始するため、未定義の動作が発生します。

1.9p15:

... スカラーオブジェクトの副作用が相対的に順序付けられていない場合同じスカラーオブジェクトの別の副作用または同じスカラーオブジェクトの値を使用した値の計算、動作は未定義です。


あなたの質問/コメントの次のステートメントはあなたの誤解の根源であるようです:

(2)割り当てのLHSも左辺値であるため、その値の評価には、iの現在の値のフェッチは含まれません。

値のフェッチは、prvalue評価の一部にすることができます。ただし、E + = Fでは、prvalueはFのみであるため、Eの値をフェッチすることは(左辺値)部分式Eの評価の一部ではありません。

式が左辺値または右辺値の場合、この式がどのように評価されるかについては何もわかりません。一部の演算子は、オペランドが右辺値を必要とするため、左辺値を必要とします。

条項5p8:

Glvalue式が、そのオペランドのprvalueを期待する演算子のオペランドとして表示される場合は常に、左辺値から右辺値(4.1)、配列からポインター(4.2)、または関数からポインター(4.3)の標準変換が行われます。式をprvalueに変換するために適用されます。

単純な割り当てでは、LHSの評価には、オブジェクトのIDを判別するだけで済みます。ただし、_+=_などの複合代入では、LHSは変更可能な左辺値である必要がありますが、この場合のLHSの評価は、オブジェクトのIDの決定と左辺値から右辺値への変換で構成されます。 RHSの評価の結果(また、prvalue)に追加されるのは、この変換(prvalue)の結果です。

「しかし、E + = Fでは唯一のprvalueはFであるため、Eの値をフェッチすることは(左辺値)部分式Eの評価の一部ではありません。」

上で説明したように、それは真実ではありません。あなたの例では、Fはprvalue式ですが、Fはlvalue式でもかまいません。その場合、左辺値から右辺値への変換はFにも適用されます。上で引用した5.17p7は、複合代入演算子のセマンティクスが何であるかを示しています。標準では、_E += F_のbehaviorは_E = E + F_と同じですが、Eは1回だけ評価されると規定されています。ここで、Eの評価には、左辺値から右辺値への変換が含まれます。これは、二項演算子_+_ではオペランドが右辺値である必要があるためです。

1
MWid

コンパイラの作成者の観点からは、彼らは"i += ++i + 1"を気にしません。なぜなら、コンパイラが何をしても、プログラマは正しい結果を得ることができないかもしれませんが、彼らは確かに彼らが値するものを得るからです。そして、誰もそのようなコードを書きません。コンパイラの作成者が気にするのは

*p += ++(*q) + 1;

コードは*p*qを読み取り、*qを1増やし、*pを計算された量だけ増やす必要があります。ここで、コンパイラの作成者は、読み取りおよび書き込み操作の順序の制限に注意を払います。明らかに、pとqが異なるオブジェクトを指している場合、順序に違いはありませんが、p == qの場合、違いが生じます。繰り返しますが、コードを書くプログラマーが気が狂っていない限り、pqとは異なります。

コードを未定義にすることで、この言語により、コンパイラーは、非常識なプログラマーを気にすることなく、可能な限り最速のコードを生成できます。コードを定義することにより、言語はコンパイラーに、非常識な場合でも標準に準拠するコードを生成させ、実行速度を低下させる可能性があります。コンパイラの作成者も正気のプログラマも、それは好きではありません。

したがって、動作がC++ 11で定義されている場合でも、(a)コンパイラがC++ 03の動作から変更されない可能性があり、(b)Cで未定義の動作である可能性があるため、これを使用することは非常に危険です。 ++ 14、上記の理由により。

0
gnasher729