質問

次の C++ 標準 ISO/IEC 14882:2003(E) の引用 (セクション 5、段落 4) を考慮してください。

指摘されている場合を除き、個々の演算子の手術の評価の順序と個々の表現のサブエクスペリション、および副作用が発生する順序は特定されていません。53)前のシーケンスポイントと次のシーケンスポイントの間に、スカラーオブジェクトは、式の評価により、最大で保存された値を変更するものとします。さらに、前の値は、保存する値を決定するためにのみアクセスするものとします。この段落の要件は、完全な表現のサブエクスペリションの許容順序ごとに満たされるものとします。それ以外の場合、動作は未定義です。[例:

i = v[i++];  // the behavior is unspecified 
i = 7, i++, i++;  //  i becomes 9 

i = ++i + 1;  // the behavior is unspecified 
i = i + 1;  // the value of i is incremented 

—終わりの例]

びっくりしました i = ++i + 1 未定義の値を返します i。を与えないコンパイラ実装を知っている人はいますか? 2 次の場合は?

int i = 0;
i = ++i + 1;
std::cout << i << std::endl;

問題はそれです operator= 引数が 2 つあります。最初のものはいつも i 参照。この場合、評価の順序は重要ではありません。C++ 標準のタブー以外に問題はありません。

お願いします, 、 する ない 引数の順序が評価にとって重要であるようなケースを考慮してください。例えば、 ++i + i 明らかに未定義です。私の場合だけ考えてくださいi = ++i + 1.

なぜ C++ 標準ではそのような表現を禁止しているのでしょうか?

役に立ちましたか?

解決

あなたは次のように考えるのは間違いです operator= 2 引数の関数として, ここで、引数の副作用は関数が開始される前に完全に評価される必要があります。そうだとしたら、この表現は、 i = ++i + 1 複数のシーケンス ポイントがあり、 ++i 課題が開始される前に完全に評価されます。しかし、そうではありません。では何が評価されているのか 本質的な 代入演算子, 、ユーザー定義の演算子ではありません。その式にはシーケンス ポイントが 1 つだけあります。

結果++i は代入の前 (および加算演算子の前) に評価されますが、 副作用 必ずしもすぐに適用されるわけではありません。結果として ++i + 1 いつも同じです i + 2, 、つまり、それが割り当てられる値です i 代入演算子の一部として。結果として ++i いつも i + 1, 、それが割り当てられるものです i インクリメント演算子の一部として。どの値を最初に割り当てるかを制御するシーケンス ポイントはありません。

このコードは、「前のシーケンス ポイントと次のシーケンス ポイントの間で、スカラー オブジェクトは式の評価によって格納されている値を最大 1 回変更する」というルールに違反しているため、動作は未定義です。 実質的に, ただし、おそらくどちらかです i + 1 または i + 2 最初に値が割り当てられ、次に他の値が割り当てられ、最後にプログラムは通常どおり実行を継続します。鼻の悪魔やトイレの爆発は発生せず、 i + 3, 、 どちらか。

他のヒント

への書き込みが 2 つあるため、これは (単に) 未指定の動作ではなく、未定義の動作です。 i 介在するシーケンスポイントなし。標準で指定されている限り、定義によりこのようになります。

この標準では、コンパイラがストレージへの書き戻しを遅らせるコードを生成すること、または別の観点からは、シーケンス ポイントの要件に準拠している限り、任意の方法で副作用を実装する命令を再順序付けするコードを生成することができます。

このステートメント式の問題は、への 2 つの書き込みを意味することです。 i シーケンスポイントが介在しない場合:

i = i++ + 1;

1 回の書き込みは、元の値の値に対して行われます。 i 「プラス 1」、もう 1 つはその値に対して再び「プラス 1」です。これらの書き込みは、標準が許可する限り、任意の順序で発生したり、完全に爆発したりする可能性があります。理論的には、これにより、同時アクセス エラーをわざわざチェックすることなく、実装にライトバックを並行して実行できる自由が与えられます。

C/C++ では、と呼ばれる概念が定義されています。 シーケンスポイント, 、これは、以前の評価のすべての効果がすでに実行されていることが保証されている実行時点を指します。言ってる i = ++i + 1 増加するため未定義です i そしてまた割り当てます i どちらも単独で定義されたシーケンス ポイントではありません。したがって、どちらが最初に起こるかは不特定です。

C++11 のアップデート (2011/09/30)

停止, 、 これは 明確に定義された C++11では。C++03 のみ未定義でしたが、C++11 ではより柔軟です。

int i = 0;
i = ++i + 1;

その行の後に、 i 2になります。この変更の理由は...なぜなら、実際にはすでに機能しており、C++11 のルールで定義したままにするよりも未定義にするほうが手間がかかるからです (実際、これが機能するようになったのは、意図的な変更というよりは偶然です。 お願いします しないでください コード内で実行してください!)。

馬の口から真っ直ぐに

http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#637

2 つの選択肢が与えられます:定義済みまたは未定義、どちらを選択しますか?

この規格の作成者には 2 つの選択肢がありました。動作を定義するか、未定義として指定します。

そもそもこのようなコードを記述することが明らかに賢明ではないことを考えると、その結果を指定することは意味がありません。そのようなコードを推奨するのではなく、阻止したいと考える人もいるでしょう。それは何の役にも立たず、何の必要もありません。

さらに、標準委員会にはコンパイラ作成者に何かを強制する方法はありません。特定の動作が必要だった場合、その要件は無視された可能性があります。

実際的な理由もありますが、それらは上記の一般的な考慮事項に従属するものではないかと思います。ただし、記録のために言っておきますが、この種の式および関連する種類に必要な動作は、コンパイラのコード生成、共通部分式の除外、レジスタとメモリ間でのオブジェクトの移動などの機能を制限します。C はすでに視界制限が弱いという障害を抱えていました。Fortran のような言語は、エイリアス化されたパラメーターとグローバルが最適化を阻害するものであることにずっと前に気づき、単純にそれらを禁止したのだと思います。

特定の式に興味があるのはわかりますが、特定の構成要素の正確な性質はあまり重要ではありません。複雑なコードジェネレーターが何を行うかを予測するのは簡単ではないため、言語は愚かな場合にはそれらの予測を必要としないように努めます。

この規格の重要な部分は次のとおりです。

格納されている値は式の評価によって最大 1 回変更される

値を 2 回変更します (1 回は ++ 演算子で、もう 1 回は代入で)

標準のコピーは古く、サンプルの 1 行目と 3 行目のコード行に既知の (そして修正された) エラーが含まれていることに注意してください。以下を参照してください。

C++ 標準コア言語の問題の目次、リビジョン 67、#351

そして

アンドリュー・ケーニッヒ:シーケンスポイントエラー:不特定ですか、それとも未定義ですか?

標準を読むだけではこのトピックを理解するのは簡単ではありません (この場合は非常に曖昧です:()。

たとえば、それが適切に定義されているか、未指定であるか、またはそうでない場合は、実際にはステートメントの構造だけでなく、実行時のメモリの内容 (具体的には変数値) にも依存します。別の例:

++i, ++i; //ok

(++i, ++j) + (++i, ++j); //ub, see the first reference below (12.1 - 12.3)

ぜひご覧ください(すべてが明確かつ正確に説明されています)。

JTC1/SC22/WG14 N926「シーケンスポイント解析」

また、Angelika Langer にはこのテーマに関する記事があります (ただし、前の記事ほど明確ではありません)。

「C++ におけるシーケンス ポイントと式の評価」

ロシア語での議論もありました(ただし、コメントや投稿自体には明らかに間違った記述がいくつかありました)。

「Точки следования (シーケンスポイント)」

次のコードは、間違った (予期しない) 結果がどのように得られるかを示しています。

int main()
{
  int i = 0;
  __asm { // here standard conformant implementation of i = ++i + 1
    mov eax, i;
    inc eax;
    mov ecx, 1;
    add ecx, eax;
    mov i, ecx;

    mov i, eax; // delayed write
  };
  cout << i << endl;
}

結果として 1 が出力されます。

「なぜ言語はこのように設計されているのですか?」と疑問に思っているとします。

あなたはそう言います i = ++i + i 「明らかに未定義」ですが、 i = ++i + 1 去るべきです i 定義された値で?率直に言って、それはあまり一貫性がありません。私は、すべてを完全に定義するか、すべてを一貫して未指定にするかのどちらかを好みます。C++ では後者があります。それ自体はそれほど悪い選択ではありません。まず、同じ「ステートメント」に 5 つまたは 6 つの変更を加える邪悪なコードを作成するのを防ぐことができます。

類推による議論: 演算子を関数の型として考えると、ある程度は理解できます。オーバーロードされたクラスがある場合 operator=, の場合、代入ステートメントは次のようなものと同等になります。

operator=(i, ++i+1)

(最初のパラメータは実際には、 this ポインタですが、これは単なる説明のためです。)

単純な関数呼び出しの場合、これは明らかに未定義です。最初の引数の値は、2 番目の引数がいつ評価されるかによって異なります。ただし、プリミティブ型の場合は、元の値が i 単に上書きされるだけです。その価値は関係ありません。でももしあなたが自分で他の魔法をやっていたら operator=, 、その後、違いが表面化する可能性があります。

簡単に言えば:すべての演算子は関数のように動作するため、同じ概念に従って動作する必要があります。もし i + ++i が未定義の場合、 i = ++i も未定義である必要があります。

このようなコードは絶対に書かないということに全員が同意するのはどうでしょうか?コンパイラがあなたが何をしたいのかを理解していない場合、あなたの後ろを追いかけている哀れな樹液があなたが何をしたいのかをどのようにして理解できると期待できますか?i++ を入れます;独自のラインで ない あなたを殺してください。

根本的な理由は、コンパイラーが値の読み取りと書き込みを処理する方法にあります。コンパイラは中間値をメモリに保存し、実際に式の終わりの値のみをコミットすることができます。式を読みます ++i 「増加」として i 1 つずつ増やして返します」と考えられますが、コンパイラはそれを「の値をロードする」とみなす可能性があります。 i, 1 つ追加して返し、誰かが再度使用する前にメモリにコミットして戻します。コンパイラは、プログラムの速度が低下するため、実際のメモリ位置への読み取り/書き込みをできるだけ避けることが推奨されます。

具体的なケースでは、 i = ++i + 1, 、一貫した行動ルールの必要性が主な原因です。多くのコンパイラはそのような状況でも「正しいこと」を行いますが、次のいずれかが is は実際にはポインターであり、 i?このルールがないと、コンパイラーはロードとストアを正しい順序で実行するために細心の注意を払う必要があります。このルールは、より多くの最適化の機会を可能にするために役立ちます。

同様のケースは、いわゆる厳密なエイリアシング ルールのケースです。値を割り当てることはできません (たとえば、 int)無関係な型の値(たとえば、 float)一部の例外を除きます。これにより、コンパイラーは、 float * 使用すると、の値が変わります int, 、最適化の可能性が大幅に向上します。

ここでの問題は、標準ではコンパイラが実行中にステートメントの順序を完全に変更できることです。ただし、ステートメントの順序を変更することは許可されていません (そのような順序変更によってプログラムの動作が変更される場合に限ります)。したがって、次の式は、 i = ++i + 1; 次の 2 つの方法で評価できます。

++i; // i = 2
i = i + 1;

または

i = i + 1;  // i = 2
++i;

または

i = i + 1;  ++i; //(Running in parallel using, say, an SSE instruction) i = 1

これは、ユーザー定義型が混在している場合にさらに悪化します。++ 演算子は、型の作成者が望む型にあらゆる影響を与える可能性があり、この場合、評価で使用される順序が重要になります。

i = v[i++];// 動作は不定です
i = ++i + 1;// 動作は不定です

上記の式はすべて、未定義の動作を呼び出します。

i = 7、i++、i++;// 私は9になります

これで大丈夫です。

Steve Summit の C-FAQ を読んでください。

から ++i, 、「1」を割​​り当てる必要がありますが、 i = ++i + 1, 、値「2」を割り当てる必要があります。介在するシーケンス ポイントがないため、コンパイラは同じ変数が 2 回書き込まれていないと想定できるため、この 2 つの操作は任意の順序で実行できます。したがって、最終値が 1 であれば、コンパイラは正しいことになります。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top