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




未定義動作 英語 (4)

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

(1) ++iが左辺値を返すが、 +が右辺値をオペランドとして取るので、左辺値から右辺値への変換が行われなければならないという微妙な説明であることがわかります。 これは、そのlvalueの現在の値を( iの古い値よりもむしろ1よりもむしろ)得ることを必要とするので、インクリメントからの副作用のにシーケンスさなければならない(すなわち、 i更新する)(2)割り当てのLHSもその値評価はi現在の値を取り出すことを伴わない。 (3)代入自体の値計算はi (更新)を更新することを伴うが、そのRHSの値計算の後にシーケンスされ、したがって、その値計算はRHSの値計算では順序付けされない。 i 、 問題ない。

UBはありません。 今私の質問は、== += (または類似の演算子)からassigment演算子を変更した場合です。

i += ++i + 1の評価は未定義の動作につながるか?

私が見ているように、標準はここに矛盾しているようです。 +=のLHSはまだ左辺値である(そしてそのRHSは依然としてプライベート値である)ので、(1)と(2)に関する限り、上記と同じ推論が適用される。 +=オペランドの評価で未定義の動作はありません。 (3)に関しては、複合割当+= (より正確には、その操作の副作用;必要に応じて、その副作用の後に値の計算が行われる)は、現在、 i現在の値(明示的に言及していなくても明示的に順序付けされていなければ、そうでなければそのような演算子の評価は常に未定義の振る舞いを呼び出す)、RHSを追加して結果をi戻します。 ++副作用で順序付けされていないが、上で議論されているように、これらの操作は未定義の動作を与えていたであろう( ++副作用は、 +=演算子の+=その値計算は、その化合物割り当ての操作の前に順序付けられる)、そうではない。

しかし一方で、標準では、 E += FE = E + Fと等価であるが、(左辺)Eは一度しか評価されないことを除いて、 ここで私の例では、 iの値の計算(これはEがここにあります) は左辺値が他のアクションでシーケンスされる必要があるものを含まないため、1回または2回実行することで違いはありません。 我々の表現はE = E + Fと厳密に等しくなければならない。 しかし、ここに問題があります。 i = i + (++i + 1)を評価すると未定義の動作が得られることは明らかです。 何がありますか? それとも標準の欠陥ですか?

追加されました。 私は上記の議論を少し変更して、副作用と価値計算との適切な区別をより正義にし、両者を包含する表現の「評価」(標準と同じ)を使用しました。 私の主な尋問は、この例では行動が定義されているかどうかだけではなく、これを決めるために標準をどのように読まなければならないかということです。 注目すべきことに、化合物の代入操作のセマンティクス(この例では明らかにUBを持つ)のための最終的な権限としてE op= F等価性をE = E op F取るか、単に何の数学的操作は、代入される値(すなわち、左辺オペランドとしての複合代入演算子の左辺値と右辺オペランドの左辺値を左オペランドとして変換した左辺値と右値との和であるopによって識別される値)を決定することに関与する。 後者の選択肢は、私が説明しようとしたように、この例でUBを議論することをはるかに難しくしています。 私は、同義を権威あるものにすることが魅力的であることを認めている(化合物の割り当ては第一級のプリミティブの一種になり、その意味はファーストクラスのプリミティブの書き換えによって与えられ、言語定義は単純化される)これに対してかなり強い主張です。

  • Eは一度しか評価されない」例外のため、等価は絶対的ではありません。 この例外は、 Eの評価が、例えばかなり一般的なa[i++] += b;ような、副作用の未定義の振る舞いを伴う場合には、使用を避けるために不可欠であることに注意してくださいa[i++] += b; 使用法。 事実、化合物の割り当てを排除するために全く同じ書き換えが可能ではないと私は考えている。 架空の|||を使って オペレータは順序付けられていない評価を指定するために、 E op= F;を定義しようとするかもしれないE op= F; (単純化のためにintオペランドを用いて) { int& L=E ||| int R=F; L = L + R; } { int& L=E ||| int R=F; L = L + R; } { int& L=E ||| int R=F; L = L + R; }しかし、この例ではもはやUBがありません。 いずれにしても、この標準は私たちに再構成レシピを与えません。

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

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

  • 化合物の割り当てが単なる割り当ての簡略化に過ぎないようにする意図があれば、この記述に明示的に含める理由はない。 最終的なフレーズは、同等性が正式であるとされた場合の事例と直接矛盾する。

化合物の割り当てがそれ自身の意味論を持っていることを認めれば、その評価は(副作用(代入)と代入後に順序付けられた)評価よりも(数学的演算を除いて) LHSの(前の)値をフェッチする名前のない操作です。 これは通常 "左辺値 - 右辺値変換"の見出しの下で扱われますが、右辺値を右辺オペランドとして取る演算子が存在しないため、ここで行うことは難しいです(ただし、展開された"同等の"形式)。 ++副作用との潜在的に順序付けられていない関係がUBの原因となるのはこの名前のない操作ですが、無名の操作はそうではないため、標準では明示されていません。 非常に存在が標準に暗にしかない操作を使用してUBを正当化することは困難です。


はい、それはUBです!

あなたの表現の評価

i += ++i + 1

次の手順で進めます。

5.17p1(C ++ 11)の州(強調する地雷):

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

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

1.9p12は答えを与える:

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

コードで複合代入演算子が使用されているので、5.17p7はこの演算子の動作を教えてくれます。

E1 op= E2形式の式の動作は、E1が1回だけ評価されるE1 = E1 op E2 except thatE1 = E1 op E2 except thatと等価です。

したがって、式E1 ( == i)の評価は、 iで指定されたオブジェクトのアイデンティティを決定し、そのオブジェクトに格納された値をフェッチするための左辺値右辺値の変換の両方を含む。 しかし、2つのオペランドE1およびE2の評価は、互いに関してE2ていない。 したがって、 E2 ( == ++i + 1)の評価が副作用( i更新するE2 ( == ++i + 1)開始するので、 未定義の動作が得られます。

1.9p15:

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

あなたの質問/コメントの次の文はあなたの誤解の根源です。

(2)代入のLHSも左辺値であるため、その値評価はi現在の値を取り出すことを伴わない

値を取得することは、評価値評価の一部となり得る。 しかし、E + = Fでは、唯一の正値はFなので、Eの値を取り出すことは、(左辺)部分式Eの評価の一部ではありません

式が左辺値または右辺値の場合、この式の評価方法については何も教えていません。 オペレータの中には、それらのオペランドとして左辺値が必要なものがあります。

第5p8項:

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

簡単な割り当てでは、LHSの評価は、オブジェクトのアイデンティティの決定のみを必要とする。 しかし、 +=などの複合割り当てでは、LHSは変更可能な左辺値でなければなりませんが、この場合のLHSの評価は、オブジェクトの識別と左辺値と右辺値の変換からなります。 これは、RHSの評価の結果(さらには評価値)に追加されるこの変換の結果(PRVUE)です。

しかし、E + = Fでは、唯一の正値はFなので、Eの値を取り出すことは、(左辺)部分式Eの評価の一部ではありません。

私が上で説明したように、それは当てはまりません。 あなたの例では、 Fはprvalue式ですが、 Fはlvalue式でもあります。 この場合、左辺値と右辺値の変換はFも適用されます。 上で引用した5.17p7は、複合代入演算子のセマンティクスを教えてくれます。 標準では、 E += F 挙動E = E + Fと同じであるが、 Eは一度しか評価されないと述べている。 ここで、 Eの評価には、左辺値と右辺値の変換が含まれます。これは、2項演算子+は、その値が右辺値である必要があるためです。


表現:

i += ++i + 1

定義されていない動作を呼び出します。 言語弁護士の方法では、次のような欠陥報告に戻る必要があります。

i = ++i + 1 ;

欠陥レポート637であるC ++ 11でよく定義されるようになりました。シーケンシングのルールと例は一致しません。

1.9 [intro.execution]パラグラフ16では、未定義ビヘイビアの例として、次の式がまだ列挙されています。

i = ++i + 1;

しかし、新しいシーケンシングルールは、この式を明確に定義しているようです

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

  1. 代入副作用は、LHSとRHS(5.17 [expr.ass]パラグラフ1)の両方の値計算の後で順序付けされる必要があります。

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

  3. RHS(++ i + 1)を値計算するためには、先ずlvalueの式++ iを値計算し、その結果に対して左辺値と右辺値の変換を行う必要があります。 これは、増分副作用が加算演算の計算の前に順序付けられ、加算演算が代入副作用の前に順序付けられることを保証する。 言い換えれば、この式の順序と最終値が明確に定義されています。

だからこの質問で私たちの問題はRHSを変えます:

++i + 1

に:

i + ++i + 1

ドラフトC ++ 11の標準セクション5.17 割り当てと複合代入演算子のために:

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

だから今、私たちは、 RHSiの計算が++iに対してRHSていないので、未定義の振る舞いを持つ状況があります。 これは、セクション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 ;
}

これは、 -Wunsequencedの clang'sテストスイートclang's明示的なテストの例によってさらに補強されています

 a += ++a; 

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

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

コードが読まなければならない*p*q、増加*q1、及び増加*p算出されるいくつかの量。ここでは、コンパイラライターは、読み取りおよび書き込み操作のための制限を気に。明らかに異なるオブジェクトにpとq点あれば、注文は違いはありませんが、あればp == q、それは違いを生むだろう。ここでも、p異なるだろうqコードを書くプログラマは正気でない限り。

コードは未定義にすることで、言語はコンパイラが非常識なプログラマのための世話をすることなく、最速のコードを生成することができます。定義されたコードにすることで、言語も、それが遅くなることがあり非常識な場合、標準に準拠したコードを生成するコンパイラを強制します。どちらのコンパイラの作家と正気のプログラマはそれを好きではありません。

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


i = ++i + 1の記述について

微妙な説明は

(1)式++iは左辺値を返すが、 +はオペランドとして右辺値を取るため、左辺値から右辺値への変換を行わなければならない。

おそらく、 CWGのアクティブな問題1642を参照してください。

これは、そのlvalueの現在の値を( iの古い値よりも1つより多くではなく)得ることを含み、従って、インクリメントからの副作用の後にシーケンスされなければならない(すなわち、 i更新する)

++iの変更)の副作用は、式++i全体の値計算の前に順序付けされる。 後者はiの値ロードするのではなく++iの結果計算することを指します。

(2)代入のLHSも左辺値であるため、その値評価はi現在の値を取り出すことを伴わない。 この値の計算はRHSの値計算では決定されませんが、これは問題ありません

私はそれがスタンダードで適切に定義されているとは思わないが、私は同意するだろう。

(3)割り当て自体の値計算は、 i (更新)を更新すること、

i = exprの値の計算は、結果を使用する場合にのみ必要です。例えば、 int x = (i = expr); または(i = expr) = 42; 。 値の計算自体はi変更しません。

i = exprために起こる式i = exprにおけるiの修正は、 = 副作用と呼ばれる。 この副作用は、 i = expr - の値計算の前にi = exprますi = expr の値計算は、 i = exprの代入の副作用の後にシーケンスされます。

一般に、式のオペランドの値の計算は、もちろんその式の副作用の前に順序付けされます。

そのRHSの値計算の後、したがってiへの前回の更新の後にシーケンスされる。 問題ない。

代入i = expr 副作用は、被演算子i (A)の値計算​​と代入のexprの後で順序付けされます。

この場合のexprは、 + expr1 + 1です。 この式の値の計算は、そのオペランドexpr1および1値計算の後にシーケンスされます。

ここでexpr1++iです。 ++iの値計算は、 ++i++iの修正)(B)の副作用の後にシーケンスされ++i

そのため、 i = ++i + 1は安全です。(A)の値の計算と(B)の同じ変数の副作用の間には、 一連の連鎖があります。

(a) Standardは++exprexpr += 1で定義しており、 expr = expr + 1として定義され、 expr = expr + 1expr = expr + 1回だけ評価される。

このexpr = expr + 1ために、 expr値計算は1つだけです。 =副作用は、 expr = expr + 1全体の値計算の前に順序付けされ、オペランドexpr (LHS)とexpr + 1 (RHS)の値計算​​の後にシーケンスされます。

これは++exprに対して++exprの値計算の前に副作用が++exprられているという私の主張に対応しています。

i += ++i + 1

i += ++i + 1の値計算には未定義の動作が含まれていますか?

+=のLHSはまだ左辺値である(そしてそのRHSは依然としてプライベート値である)ので、(1)と(2)に関する限り、上記と同じ推論が適用される。 (3)のように、 +=演算子の値の計算では、現在のi値をフェッチする必要があります(標準があまり明示的でなくても明示的に後続するか、そうでなければそのような演算子の実行は常に未定義のビヘイビアを呼び出す)、RHSの追加を実行し、結果をi戻します。

私はここに問題があると思います: i += LHSにi +=加えると、 ++i + 1結果に、 ++i + 1の値を知ることが必要になります - 値の計算( iの値をロードすることを意味する)。 この値の計算は、 ++iによって実行される修正に関して順序付けされていない。 これは、基本的なi += expr - > i = i + expr書かれた書き換えに続いて、あなたの代わりの記述で言うことです。 ここで、 i + expr内のi + exprの値計算は、 expr値計算に関して順序付けされていない。 それがあなたがUBになる場所です

値の計算には、オブジェクトの「アドレス」またはオブジェクトの値という2つの結果が含まれることに注意してください。 式i = 42では、lhsの値の計算によってiアドレスが生成されます。 つまり、コンパイラは(抽象マシンの観察可能な振る舞いのルールのもとで)rhsをどこに格納するかを把握する必要があります。 式i + 42では、 i + 42の値計算によって値が生成されます。 上記の段落では、私は第2種を参照していたので、[intro.execution] p15が適用されます。

スカラオブジェクトの副作用が、同じスカラーオブジェクトの別の副作用または同じスカラーオブジェクトの値を使用する値計算と比較して順序付けされていない場合、その動作は未定義です。

i += ++i + 1別のアプローチ

+=演算子の値の計算は、現在iの現在の値をフェッチし、 [...] RHSの加算を実行する必要があります

RHSは++i + 1です。 この式の結果(値の計算)を計算することは、LHSからのiの値計算に関して順序付けされていません。 だから、この文の中の言葉は誤解を招きます。もちろん、最初にiをロードしてから、RHSの結果をそれに加えなければなりません。 しかし、RHSの副作用とLHSの価値を得るための価値計算との間には順序がありません。 たとえば、RHSによって変更されたように、LHSの古い値または新しい値のいずれかを得ることができます。

一般に、ストアと「並行」ロードはデータ競合であり、未定義動作につながります。

補遺に取り組む

架空の|||を使って オペレータは順序付けられていない評価を指定するために、 E op= F;を定義しようとするかもしれないE op= F; (単純化のためにintオペランドを用いて) { int& L=E ||| int R=F; L = L + R; } { int& L=E ||| int R=F; L = L + R; } { int& L=E ||| int R=F; L = L + R; }しかし、この例ではもはやUBがありません。

EiF++i (私たちは+ 1は必要ありません)としましょう。 そして、 i = ++i

int* lhs_address;
int lhs_value;
int* rhs_address;
int rhs_value;

    (         lhs_address = &i)
||| (i = i+1, rhs_address = &i, rhs_value = *rhs_address);

*lhs_address = rhs_value;

一方、 i += ++i

    (         lhs_address = &i, lhs_value = *lhs_address)
||| (i = i+1, rhs_address = &i, rhs_value = *rhs_address);

int total_value = lhs_value + rhs_value;
*lhs_address = total_value;

これは、シーケンシングの保証に対する私の理解を表すことを意図しています。 演算子は、RHSの値計算および副作用のすべてを、RHSの値計算の前に順序付けることに注意してください。 括弧はシーケンス処理には影響しません。 第2のケースでは、 i += ++iでは、 i => UBの左辺値から右辺値への変換が行われます。

この標準は、セマンティクスの別個の定義が必要でない第2クラスのプリミティブとしての複合割り当てを扱っていない。

私はそれが冗長であると言います。 E1 op = E2からE1 = E1 op E2 E1 op = E2への書き換えには、必要な式の種類と値のカテゴリ(rhsでは5.17 / 1がlhsについて何かを述べています)、ポインタの種類、必要な変換などが含まれます。悲しいことは、5.17 / 1の「..に関して」という文は、その等価性の例外として5.17 / 7にないということです。

いずれにしても、複合割り当てと単純割り当てとオペレータの保証と要件を比較し、矛盾がないかどうかを確認する必要があります。

私たちが5.17 / 7の例外のリストにも「..との関係で」と書いたら、私は矛盾があるとは思わない。

それが判明したとき、Marc van Leeuwenの答えの議論で見ることができるように、この文は次の興味深い観察につながります:

int i; // global
int& f() { return ++i; }
int main() {
    i  = i + f(); // (A)
    i +=     f(); // (B)
}

fの本文の評価はi + f()値計算と不確定に順序付けられているので、(A)は2つの可能な結果を​​有するように思われる。

一方、(b)では、 f()本体の評価は、 +=値計算の前に順序付けされます。なぜなら+=は単一の操作として見なければならないからです。 +=代入。





undefined-behavior