関数 - c++ 抽象 化




なぜf(i=-1、i=-1)の未定義の振る舞いですか? (8)

私は評価違反の命令について読んでいました。彼らは私に困惑する例を挙げています。

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

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

このコンテキストでは、 iスカラーオブジェクトです 。これは明らかに

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

私は、その場合のステートメントがどのように曖昧であるかは分かりません。 第1引数または第2引数が最初に評価されるかどうかにかかわらず、 i-1として終了し、両方の引数も-1です。

誰かが明確にしてもらえますか?

更新

私はすべての議論に本当に感謝しています。 これまでのところ、私は@ harmicの答えがとても好きです。一見すると、まっすぐ前方に見えるにもかかわらず、この声明を定義する落とし穴と複雑さを露呈しているからです。 @acheong87 、参照を使用するときに出てくるいくつかの問題を指摘していますが、私はそれがこの質問の順序付けられていない副作用の側面に直面していると思います。

概要

この質問には多大な注意が払われているので、主なポイント/答えを要約します。 第一に、「なぜ」は、「何のために 」、「どのような理由で 」、「どのような目的の ために 」、密接に関連していて微妙に異なる意味を持つことができるということを指摘してください。 私は、彼らが対処した「なぜ」という意味のものをグループ化します。

何が原因なのか

ここでの主な答えはポール・ドレイパー ( Paul Draper )ですが、 マーティンJはこれに類似していますが広範な答えはありません。 ポールドレイパーの答えは

それは振る舞いが定義されていないため、未定義の動作です。

答えは全体的にC ++標準が何を説明しているかという点で全体的に非常に良いです。 また、 f(++i, ++i);ようなUBのいくつかの関連するケースを扱いますf(++i, ++i); f(i=1, i=-1); 。 関連する最初のケースでは、最初の引数がi+1で、2番目の引数が2でなければならないかどうか、またはその逆であるかどうかは不明です。 2番目の関数呼び出しの後に1または-1にする必要があるかどうかは不明です。 これらのケースは両方とも、以下のルールに該当するためUBです。

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

したがって、 f(i=-1, i=-1)もUBである。なぜなら、プログラマの意図は(IMHO)明白で曖昧ではないにもかかわらず、同じルールの下にあるからである。

Paul Draperはまた、彼の結論において、

動作が定義されていますか? はい。 それは定義されましたか? いいえ。

「どの理由/目的がf(i=-1, i=-1)が未定義の振る舞いとして残っているのか」の問題に私たちを導いてくれます。

どんな理由/目的のために

C ++標準にはいくつかの見落とし(多分不注意)がありますが、多くの省略は十分に推論され、特定の目的に役立ちます。 私は、目的が "コンパイラライターの仕事をより簡単にする"か、 "より速いコード"にすることが多いと知っていますが、 f(i=-1, i=-1) をUBとする。

harmicとsupercatは、UBの理由を提供する主要な答えを提供します。 Harmicは、表面上原子的な割り当て操作を複数の機械命令に分割し、それらの命令をさらに最適な速度でインターリーブする可能性のある最適化コンパイラを指摘しています。 これはいくつかの非常に驚くべき結果につながる可能性があります。 iは彼のシナリオでは-2に終わります! したがって、操作が順序付けされていない場合、 同じ値を複数回変数に代入する悪影響を被る可能性があります。

supercatはf(i=-1, i=-1)を得ようとする落とし穴の関連する説明を提供しなければなりません。 彼は、いくつかのアーキテクチャでは、同じメモリアドレスへの複数の同時書き込みに対する厳しい制限があることを指摘しています。 私たちがf(i=-1, i=-1)よりも些細なことを扱っていたなら、コンパイラはこれをキャッチするのが難しいかもしれません。

davidfまた、harmicのものと非常によく似たインターリーブ命令の例を提供します。

火星、ダビデのそれぞれの例は多少の工夫がなされていますが、 f(i=-1, i=-1)が未定義の動作でなければならないという確かな理由があります。

私はダメージの答えを受け入れました。なぜなら、ポール・ドレイパーの答えが「何の原因で」の部分を改善したとしても、それが理由のすべての意味に対処する最良の仕事をしたからです。

その他の回答

JohnBは、オーバーロードされた代入演算子(単なるJohnBスカラーではなく)を考慮すると、 JohnBする可能性があることを指摘しています。


2つの値が同じであるという理由だけでルールから例外を作成しないという実用的な理由:

// config.h
#define VALUEA  1

// defaults.h
#define VALUEB  1

// prog.cpp
f(i = VALUEA, i = VALUEB);

これが許された場合を考えてみましょう。

今、数ヶ月後には、

 #define VALUEB 2

一見無害ですね。 そして突然、prog.cppはもうコンパイルされません。 しかし、コンパイルはリテラルの価値に依存すべきではないと感じています。

結論:ルールの例外はありません。これは、定数の値(型ではなく)によってコンパイルが成功するためです。

EDIT

@HeartWareはBが0であるとき、 A DIV Bという形式の定数式がいくつかの言語では許可されず、コンパイルが失敗することを指摘しました。 したがって、定数を変更すると、別の場所でコンパイルエラーが発生する可能性があります。 これは、IMHO、残念です。 しかし、このようなことをやむを得ないものに制限することは、確かに良いことです。


「役に立つ」ようにしようとしていたコンパイラが、まったく予期しない動作を引き起こすようなことをする理由が考えられる場合、動作は通常未定義として指定されます。

変数が異なる時間に書き込まれることを確実にするために変数を何度も書き込む場合、ハードウェアによっては、デュアルポートメモリを使用して複数の「ストア」演算を異なるアドレスに同時に実行できる場合があります。 しかし、デュアルポートメモリの中には、 書き込まれた値が一致しているかどうかかかわらず 、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"に渡します。 "動物園"が "v"を変更しない場合、2つの呼び出しに異なる値が渡される方法はないはずですが、それはとにかく起こる可能性があります。 一方、両方の店舗が同じ価値を書いている場合、そのような奇妙なことは起こり得ず、ほとんどのプラットフォームでは、実装が奇妙なことをする理にかなった理由はありません。 残念ながら、コンパイラライターの中には、「標準で許可されている」ために、愚かな行為の言い訳をする必要がないものもあります。


C ++ 17では、より厳しい評価ルールが定義されています。 特に、関数の引数を順序付ける(不特定の順序であるが)。

N5659 §4.6:15
評価AおよびBは、 BまたはBAより前に配列決定される前にAが配列決定されるが、どちらが特定されていないかは不明確に配列決定される。 [ :不確定に配列された評価は重複することはできませんが、いずれか最初に実行することができます。 - 終了ノート ]

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

これはちょうど "スカラーオブジェクト"がintやfloatのようなものの他に何を意味するのかわかりません。

私は "スカラー型オブジェクト"を "スカラー型オブジェクト"の省略形、または単に "スカラー型変数"と解釈します。 次に、 pointerenum (定数)はスカラー型です。

これはスカラー型の MSDN記事です。


代入演算子がオーバーロードされる可能性があります。その場合、順序は重要です。

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

実際には、コンパイラがiに同じ値を2回割り当てられていることをチェックするという事実に依存しない理由があります。そのため、1回の代入で置き換えることが可能です。 表現があればどう?

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)));
}

混乱は、ローカル変数に定数値を格納することは、Cが実行されるように設計されたすべてのアーキテクチャ上の1つのアトミック命令ではないことです。 この場合、コンパイラ以外のコードで実行されるプロセッサです。 たとえば、各命令が完全な32ビット定数を保持できないARMの場合、変数にintを格納するにはさらに多くの命令が必要です。 この擬似コードの例では、一度に8ビットしか格納できず、32ビットレジスタで動作する必要があります。私は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を得る。しかし、たとえ定数でさえそれがそうであるように明白でないかもしれないと考えることが重要であるので、私の例はまだ有効である。


関数の引数式の順序付けに関する唯一の規則がここにあるように思えます:

3)関数を呼び出すとき(関数がインラインであるかどうか、そして明示的な関数呼び出し構文が使用されているかどうかにかかわらず)、引数式または呼び出された関数を指定する後置式式に関連するすべての値の計算と副作用は呼び出された関数の本体のすべての式または文が実行される前にシーケンス化されます。

これは引数式間の順序付けを定義していないので、この場合は次のようになります。

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

実際には、ほとんどのコンパイラでは、引用した例はうまく動作します(「ハードディスクの消去」や他の理論上の未定義の動作の結果とは対照的です)。
ただし、割り当てられた2つの値が同じであっても、特定のコンパイラの動作に依存するため、負債です。 また、明らかに、異なる値を割り当てようとすると、結果は「真に」未定義になります。

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}




undefined-behavior