評価違反の順序 について読んでいたのですが、彼らは私を困惑させる例を示しています。
1)スカラーオブジェクトの副作用が同じスカラーオブジェクトの別の副作用と比較して順序付けられていない場合、動作は未定義です。
// snip f(i = -1, i = -1); // undefined behavior
このコンテキストでは、i
はスカラーオブジェクトです。
算術型(3.9.1)、列挙型、ポインター型、メンバー型へのポインター(3.9.2)、std :: nullptr_t、およびこれらの型のcv修飾バージョン(3.9.3)は、まとめてスカラー型と呼ばれます。
その場合、声明が曖昧であるかはわかりません。最初の引数と2番目の引数のどちらが最初に評価されるかに関係なく、i
は-1
になり、両方の引数も-1
になります。
誰かが明確にできますか?
すべての議論に感謝します。これまでのところ、一見一見単純に見えるにもかかわらず、このステートメントを定義する際の落とし穴と複雑さを明らかにしているので、 @ harmic’s answer が大好きです。 @ acheong87 は、参照の使用時に発生するいくつかの問題を指摘していますが、これはこの質問の順序付けられていない副作用の側面と直交すると思います。
この質問には多くの注目が集まったので、主なポイント/回答を要約します。最初に、「なぜ」が密接に関連しているが微妙に異なる意味を持つことができることを指摘する小さな余談を許可します。つまり、「for whatcause」、「what whatreason」、および「for whatpurpose」。これらの「理由」の意味がどのように対処したかによって答えをグループ化します。
ここでの主な答えは Paul Draper であり、 Martin J は同様ですが、広範な答えではありません。ポール・ドレイパーの答えは
動作が定義されていないため、未定義の動作です。
答えは、C++標準が何を言っているかを説明するという点で全体的に非常に良いです。また、f(++i, ++i);
やf(i=1, i=-1);
など、UBの関連するいくつかのケースにも対応しています。最初の関連するケースでは、最初の引数がi+1
で、2番目の引数がi+2
であるか、その逆であるかは明確ではありません。 2番目では、関数呼び出し後にi
が1であるか-1であるかが明確ではありません。これらのケースは両方ともUBです。これらのケースは次のルールに該当するためです。
スカラーオブジェクトの副作用が、同じスカラーオブジェクトの別の副作用と比較して順序付けられていない場合、動作は未定義です。
したがって、f(i=-1, i=-1)
もUBです。これは、プログラマの意図が(IMHO)明白で曖昧ではないにもかかわらず、同じ規則に該当するためです。
ポール・ドレーパーはまた、彼の結論でそれを明示的にしています。
定義された動作でしたか?はい。定義されましたか?番号。
「f(i=-1, i=-1)
が未定義の動作として残っていたのはどのような理由/目的ですか?」という質問につながります。
C++標準にはいくつかの見落とし(おそらく不注意)がありますが、多くの省略は十分な理由があり、特定の目的に役立ちます。多くの場合、目的は「コンパイラライターの仕事を楽にする」または「コードを高速化する」ことのいずれかであることを認識していますが、主な理由は、正当な理由があるかどうかを知ることですleavef(i=-1, i=-1)
UBとして
harmic および supercat は、UBにreasonを提供する主な回答を提供します。 Harmicは、表面上アトミックな割り当て操作を複数のマシン命令に分割する可能性のある最適化コンパイラ、および最適な速度のためにこれらの命令をさらにインターリーブする可能性があることを指摘しています。これはいくつかの非常に驚くべき結果につながる可能性があります:i
は彼のシナリオでは-2になります!したがって、harmicは、同じ値を変数に複数回割り当てると、操作が順序付けされていない場合に悪影響を与える可能性があることを示しています。
supercatは、f(i=-1, i=-1)
を取得して、本来あるべきように見えることをしようとする落とし穴の関連する説明を提供します。彼は、一部のアーキテクチャでは、同じメモリアドレスへの複数の同時書き込みに対して厳しい制限があることを指摘しています。 f(i=-1, i=-1)
よりも些細ではないものを扱っている場合、コンパイラはこれをキャッチするのに苦労する可能性があります。
davidf は、harmicと非常によく似たインターリーブ命令の例も提供します。
Harmic、supercat、およびdavidfの例はそれぞれ多少工夫されていますが、これらを組み合わせることで、f(i=-1, i=-1)
が未定義の振る舞いであるという明確な理由を提供できます。
ポール・ドレイパーの答えは「何のために」の部分をよりよく扱っていたとしても、私はharmicの答えを受け入れました。
JohnB は、(単なるスカラーではなく)多重定義された代入演算子を考慮すると、同様にトラブルに遭遇する可能性があることを指摘しています。
操作は順序付けられていないため、割り当てを実行する命令をインターリーブできないと言うことはできません。 CPUアーキテクチャによっては、最適な場合があります。参照ページには次のように記載されています。
AがBの前にシーケンスされておらず、BがAの前にシーケンスされていない場合、次の2つの可能性があります。
aおよびBの評価は順序付けられていません:任意の順序で実行でき、オーバーラップする場合があります(実行の単一スレッド内で、コンパイラはAおよびBを含むCPU命令をインターリーブできます)
aとBの評価は不定に順序付けられます:それらは任意の順序で実行できますが、重複することはできません:AはBの前に完了するか、BはAの前に完了します。評価されます。
それ自体は問題を引き起こすようには見えません-実行中の操作が値-1をメモリ位置に保存していると仮定します。しかし、コンパイラーはそれを同じ効果を持つ別の命令セットに最適化することはできませんが、操作が同じメモリー位置で別の操作とインターリーブされた場合に失敗する可能性があると言うこともできません。
たとえば、値-1をロードするのに比べて、メモリをゼロにしてからデクリメントする方が効率的だったと想像してください。その後、次のようになります。
f(i=-1, i=-1)
になるかもしれない:
clear i
clear i
decr i
decr i
今、私は-2です。
おそらく偽の例ですが、可能です。
まず、「スカラーオブジェクト」とは、int
、float
、またはポインターのようなタイプを意味します( C++のスカラーオブジェクトとは を参照)。
第二に、それはより明白に見えるかもしれません
f(++i, ++i);
未定義の動作になります。しかし
f(i = -1, i = -1);
それほど明白ではありません。
少し異なる例:
int i;
f(i = 1, i = -1);
std::cout << i << "\n";
「最後」、i = 1
、またはi = -1
のどの割り当てが発生しましたか?標準では定義されていません。本当に、それはi
が5
になる可能性があることを意味します(これがどのようになるかについての完全に妥当な説明については、harmicの答えを参照してください)。または、プログラムがセグメンテーション違反になる可能性があります。または、ハードドライブを再フォーマットします。
しかし、今、あなたは尋ねます:「私の例はどうですか?両方の割り当てに同じ値(-1
)を使用しました。それについて不明な点は何ですか?」
あなたは正しい... C++標準委員会がこれを説明した方法を除いて。
スカラーオブジェクトの副作用が、同じスカラーオブジェクトの別の副作用と比較して順序付けられていない場合、動作は未定義です。
彼らはcouldはあなたの特別なケースに対して特別な例外を作ったが、そうではなかった。 (そして、なぜそうする必要がありますか?どんな用途があるでしょうか?)そのため、i
は依然として5
になります。または、ハードドライブが空である可能性があります。したがって、あなたの質問に対する答えは:
動作が何であるかが定義されていないため、未定義の動作です。
(これは、多くのプログラマーが「未定義」は「ランダム」または「予測不能」を意味すると考えているため、強調に値します。そうではありません。未定義のままです。)
定義された動作でしたか?はい。定義されましたか?いいえ。したがって、「未定義」です。
つまり、「未定義」とは、コンパイラがハードドライブをフォーマットすることを意味するわけではありません...それは、couldであることを意味します。現実的には、g ++、Clang、およびMSVCがすべて期待どおりに機能すると確信しています。彼らは「しなければならない」だけです。
別の質問はかもしれません。なぜC++標準委員会はこの副作用を順序付けしないことを選択したのですか?その答えには、委員会の歴史と意見が含まれます。またはC++でこの副作用を順序付けしないことで何が良いのでしょうか?これは、標準化委員会の実際の理由であるかどうかにかかわらず、正当化を許可します。これらの質問はこちら、またはProgrammers.stackexchange.comでお問い合わせください。
2つの値が同じという理由だけで、ルールから例外を作成しない実用的な理由:
// config.h
#define VALUEA 1
// defaults.h
#define VALUEB 1
// prog.cpp
f(i = VALUEA, i = VALUEB);
これが許可された場合を考えます。
今、数ヶ月後、変更する必要が生じます
#define VALUEB 2
一見無害ですよね?それでも、突然prog.cppはコンパイルされなくなりました。しかし、コンパイルはリテラルの値に依存すべきではないと感じています。
結論:ルールの例外はありません。これは、コンパイルの成功が定数の値(タイプではなく)に依存するためです。
@ HeartWareが指摘したB
が0の場合、A DIV B
という形式の定数式は一部の言語では許可されず、コンパイルが失敗します。したがって、定数を変更すると、他の場所でコンパイルエラーが発生する可能性があります。残念ながら、これは私見です。しかし、そのようなことを避けられないものに制限することは確かに良いことです。
混乱は、定数値をローカル変数に格納することは、Cが実行されるように設計されているすべてのアーキテクチャ上の1つのアトミック命令ではないということです。この場合、コードが実行されるプロセッサは、コンパイラよりも重要です。たとえば、各命令が完全な32ビット定数を保持できないARMでは、変数にintを格納するには複数の命令が必要です。一度に8ビットしか保存できず、32ビットレジスタで動作する必要があるこの擬似コードの例、iはint32です。
reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last
コンパイラーが最適化を望む場合、同じシーケンスを2回インターリーブする可能性があり、どの値がiに書き込まれるかわからないことが想像できます。そして、彼はあまり賢くないとしましょう:
reg = 0xFF;
reg |= 0xFF00;
reg |= 0xFF0000;
reg = 0xFF;
reg |= 0xFF000000;
i = reg; // writes 0xFF0000FF == -16776961
reg |= 0xFF00;
reg |= 0xFF0000;
reg |= 0xFF000000;
i = reg; // writes 0xFFFFFFFF == -1
しかし、私のテストでは、gccは同じ値が2回使用され、1回生成され、奇妙なことは何もしないことを認識するのに十分なほど親切です。 -1、-1を取得しますが、定数であっても見かけほど明白ではない可能性があることを考慮することが重要であるため、私の例は依然として有効です。
「役に立つ」ことを試みたコンパイラがまったく予期しない動作を引き起こす可能性のある何らかの理由が考えられる場合、動作は一般に未定義として指定されます。
異なる時間に書き込みが行われることを保証するために変数が何も書き込まれない場合、ハードウェアの種類によっては、デュアルポートメモリを使用して異なるアドレスに対して複数の「ストア」操作を同時に実行できる場合があります。ただし、一部のデュアルポートメモリは、2つのストアが同じアドレスに同時にヒットするシナリオを明示的に禁止しています。書き込まれた値が一致するかどうかに関係なく。そのようなマシンのコンパイラーが、同じ変数を2回連続して書き出そうとしないことに気付いた場合、コンパイルを拒否するか、2つの書き込みが同時にスケジュールされないようにします。ただし、一方または両方のアクセスがポインターまたは参照を介している場合、コンパイラーは両方の書き込みが同じストレージ位置にヒットするかどうかを常に判断できるとは限りません。その場合、書き込みを同時にスケジュールし、アクセス試行でハードウェアトラップを引き起こす可能性があります。
もちろん、誰かがそのようなプラットフォームにCコンパイラを実装するかもしれないという事実は、アトミックに処理されるのに十分小さい型のストアを使用する場合、そのような動作をハードウェアプラットフォームで定義すべきでないことを示唆していません。コンパイラが認識していない場合、2つの異なる値をシーケンスなしで保存しようとすると、奇妙な結果になる可能性があります。たとえば、次の場合:
uint8_t v; // Global
void hey(uint8_t *p)
{
moo(v=5, (*p)=6);
Zoo(v);
Zoo(v);
}
コンパイラが「moo」の呼び出しをインライン化し、「v」を変更しないと判断できる場合、5をvに格納し、6を* pに格納し、5を「Zoo」に渡し、 vの内容を「Zoo」に渡します。 「Zoo」が「v」を変更しない場合、2つの呼び出しに異なる値を渡す方法はないはずですが、とにかく簡単に発生する可能性があります。一方、両方のストアが同じ値を書き込む場合、そのような奇妙なことは発生せず、ほとんどのプラットフォームでは、実装が奇妙なことをする賢明な理由はありません。残念ながら、一部のコンパイラライターは、「標準で許可されているため」を超える愚かな動作の言い訳を必要としないため、これらのケースでも安全ではありません。
thisの場合、ほとんどの実装で結果が同じになるという事実は偶然です。評価の順序は未定義のままです。 f(i = -1, i = -2)
を考慮してください:ここでは、順序が重要です。あなたの例で重要でない唯一の理由は、両方の値が-1
であるという偶然です。
式が未定義の振る舞いを持つものとして指定されているとすると、悪意のあるコンパイラは、f(i = -1, i = -1)
を評価して実行を中止したときに不適切な画像を表示する可能性があります。幸いなことに、私が知っているコンパイラはありません。
関数の引数式の順序付けに関する唯一のルールは次のように思えます。
3)関数を呼び出すとき(関数がインラインであるかどうか、および明示的な関数呼び出し構文が使用されるかどうか)、引数式、または呼び出された関数を指定する接尾辞式に関連付けられたすべての値の計算と副作用は、呼び出された関数の本体内のすべての式またはステートメントの実行前に順序付けられます。
これは、引数式間の順序付けを定義しないため、この場合になります。
1)スカラーオブジェクトの副作用が、同じスカラーオブジェクトの別の副作用と比較して順序付けられていない場合、動作は未定義です。
実際には、ほとんどのコンパイラで、引用した例は問題なく実行されます(「ハードディスクの消去」やその他の理論上の未定義の動作結果とは対照的です)。
ただし、割り当てられた2つの値が同じであっても、特定のコンパイラの動作に依存するため、これは問題となります。また、明らかに、異なる値を割り当てようとした場合、結果は「本当に」未定義になります。
void f(int l, int r) {
return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
formatDisk();
}
C++ 17は、より厳密な評価ルールを定義します。特に、関数引数を順序付けします(ただし、順序は指定されていません)。
N5659 §4.6:15
評価AおよびBはAの前にBまたはBはAの前にシーケンスされますが、指定されていませんどれ。 [Note:不定にシーケンスされた評価はオーバーラップできませんが、どちらかを最初に実行できます。 —メモの終了]
N5659 § 8.2.2:5
関連するすべての値の計算と副作用を含むパラメーターの初期化は、他のパラメーターの初期化に関して不確定にシーケンス化されます。
以前はUBであったいくつかのケースを許可します:
f(i = -1, i = -1); // value of i is -1
f(i = -1, i = -2); // value of i is either -1 or -2, but not specified which one
代入演算子がオーバーロードされる可能性があります。その場合、順序が重要になる可能性があります。
struct A {
bool first;
A () : first (false) {
}
const A & operator = (int i) {
first = !first;
return * this;
}
};
void f (A a1, A a2) {
// ...
}
// ...
A i;
f (i = -1, i = -1); // the argument evaluated first has ax.first == true
これは、「intやfloatなどのほかに「スカラーオブジェクト」が何を意味するのかわからない」に答えているだけです。
「スカラーオブジェクト」を「スカラータイプオブジェクト」の略語、または単に「スカラータイプ変数」と解釈します。次に、pointer
、enum
(定数)はスカラー型です。
これは Scalar Types のMSDN記事です。
実際には、コンパイラがi
に同じ値が2回割り当てられていることをチェックするという事実に依存しない理由があり、単一の割り当てに置き換えることができます。いくつかの式がある場合はどうなりますか?
void g(int a, int b, int c, int n) {
int i;
// hey, compiler has to prove Fermat's theorem now!
f(i = 1, i = (ipow(a, n) + ipow(b, n) == ipow(c, n)));
}