94

私が持っている場合:

unsigned int x;
x -= x;

x この式の後で がゼロであることは明らかですが、どこを見ても、このコードの動作は未定義であり、単にx(減算前まで) の値ではないと言われています。

2 つの質問:

  • このコードの動作は実際に未定義ですか?
    (例えば、準拠したシステムでコードがクラッシュ[またはさらに悪化]する可能性はありますか?)

  • もしそうなら、ここでゼロであるべきことが完全に明らかなのに、なぜCは動作が未定義であると言うのですか?x

    つまり、ここで動作を定義しないことによって 得られる利点は何ですか?

明らかに、コンパイラは、変数内で「便利」と見なされるガベージ値を単純に使用でき、意図したとおりに機能します...そのアプローチの何が問題なのですか?

4

7 に答える 7

95

はい、この動作は定義されていませんが、ほとんどの人が認識しているのとは異なる理由があります。

まず、初期化された値を使用すること自体は未定義の動作ではありませんが、値は単に不確定です。値がたまたまその型のトラップ表現である場合、これにアクセスするのは UB です。符号なしの型にトラップ表現があることはめったにないため、その側では比較的安全です。

動作が未定義になるのは、変数の追加のプロパティです。つまり、「で宣言できた可能性がある」ということです。つまり、registerそのアドレスは取得されません。このような変数は特別に扱われます。これは、「初期化されていない」型ドメインの値に対応しない特別な状態を持つ実際の CPU レジスタを持つアーキテクチャがあるためです。

編集:標準の関連フレーズは 6.3.2.1p2 です。

左辺値が register ストレージ クラスで宣言できた可能性のある自動ストレージ期間のオブジェクトを指定し (そのアドレスが取得されていない)、そのオブジェクトが初期化されていない (初期化子で宣言されておらず、使用前に割り当てが実行されていない) 場合)、動作は未定義です。

明確にするために、次のコードすべての状況下で有効です。

unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
  • aここではとのアドレスbが使用されるため、その値は不定です。
  • unsigned char不確定な値が指定されていないだけの表現をトラップすることは決してないため、任意の値がunsigned char発生する可能性があります。
  • 最後に値を保持するa 必要0があります。

Edit2: a未指定の値がありbます:

3.19.3未規定値
この国際規格が要件を課さない関連するタイプの有効な値。

于 2012-08-15T07:13:13.717 に答える
25

C 標準は、コンパイラに最適化を実行するための多くの自由を与えます。初期化されていないメモリがランダムなビット パターンに設定され、すべての操作が記述された順序で実行される単純なプログラム モデルを想定すると、これらの最適化の結果は驚くべきものになる可能性があります。

注: 次の例はx、アドレスが取得されないため有効なだけであり、「レジスタのような」ものです。xタイプがトラップ表現を持っている場合にも有効です。これは unsigned 型の場合はめったになく (少なくとも 1 ビットのストレージを「浪費」する必要があり、文書化する必要があります) unsigned char、. 符号付きの型がある場合、実装は -(2 n-1 -1) と 2 n-1x -1の間の数値ではないビット パターンをトラップ表現として定義できます。Jens Gustedt の回答を参照してください。

レジスタはメモリよりも高速であるため、コンパイラはレジスタを変数に割り当てようとします。プログラムは、プロセッサが持っているレジスタよりも多くの変数を使用する可能性があるため、コンパイラはレジスタ割り当てを実行します。これにより、異なる変数が異なるタイミングで同じレジスタを使用することになります。プログラムフラグメントを検討してください

unsigned x, y, z;   /* 0 */
y = 0;              /* 1 */
z = 4;              /* 2 */
x = - x;            /* 3 */
y = y + z;          /* 4 */
x = y + 1;          /* 5 */

行 3 が評価されるとき、xまだ初期化されていないため、(コンパイラの理由により) 行 3 は、コンパイラが理解できるほど賢くなかったという他の条件のために発生することのないある種のまぐれであるに違いありません。zは 4 行目以降とx5 行目以前では使用されていないため、両方の変数に同じレジスタを使用できます。したがって、この小さなプログラムは、レジスタに対する次の操作にコンパイルされます。

r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;

の最終値xは の最終値でありr0、 の最終値yは の最終値ですr1xこれらの値は x = -3 および y = -4 であり、適切に初期化された場合に発生する 5 および 4 ではありません。

より複雑な例として、次のコード フラグメントを検討してください。

unsigned i, x;
for (i = 0; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

condition副作用がないことをコンパイラが検出したとします。conditionは を変更しないため、コンパイラは、ループの最初の実行がまだ初期化されていないため、xアクセスできない可能性があることを認識しています。xしたがって、ループ本体の最初の実行は と同等x = some_value()であり、条件をテストする必要はありません。コンパイラは、あなたが書いたかのようにこのコードをコンパイルするかもしれません

unsigned i, x;
i = 0; /* if some_value() uses i */
x = some_value();
for (i = 1; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

これをコンパイラ内でモデル化する方法は、初期化されていない限り、に依存するすべての値が便利な値xを持つと見なすことです。変数が単に指定されていない値を持つのではなく、初期化されていない変数の動作が未定義であるため、コンパイラは、便利な値の間の特別な数学的関係を追跡する必要はありません。したがって、コンパイラは上記のコードを次のように分析します。x

  • 最初のループ反復中に、が評価xされるまでに初期化されません-x
  • -xには未定義の動作があるため、その値は都合のよいものです。
  • 最適化ルールが適用されるため、このコードは に簡略化できます。condition ? value : valuecondition; value

あなたの質問のコードに直面したとき、この同じコンパイラは、x = - xが評価されたときに、 の値が-x便利なものであることを分析します。したがって、割り当てを最適化して取り除くことができます。

上記のように動作するコンパイラの例を探したことはありませんが、これは優れたコンパイラが行おうとしている種類の最適化です。遭遇しても驚かない。これは、プログラムがクラッシュするコンパイラのあまり妥当でない例です。(ある種の高度なデバッグ モードでプログラムをコンパイルする場合、それほど信じがたいことではないかもしれません。)

この架空のコンパイラは、すべての変数を別のメモリ ページにマップし、ページ属性を設定して、初期化されていない変数からの読み取りがデバッガを呼び出すプロセッサ トラップを引き起こすようにします。変数への代入は、最初にそのメモリ ページが正常にマップされていることを確認します。このコンパイラは、高度な最適化を実行しようとはしません。初期化されていない変数などのバグを簡単に見つけることを目的としたデバッグ モードです。がx = - x評価されると、右側でトラップが発生し、デバッガーが起動します。

于 2012-08-15T00:51:07.330 に答える
17

はい、プログラムがクラッシュする可能性があります。たとえば、CPU 割り込みを引き起こすトラップ表現 (処理できない特定のビット パターン) があり、処理されないとプログラムがクラッシュする可能性があります。

(後半の C11 ドラフトの 6.2.6.1 は述べています) 特定のオブジェクト表現は、オブジェクト型の値を表す必要はありません。オブジェクトの格納された値がそのような表現を持ち、文字型を持たない左辺値式によって読み取られる場合、動作は未定義です。そのような表現が、文字型を持たない左辺値式によってオブジェクトのすべてまたは一部を変更する副作用によって生成される場合、動作は未定義です.50) このような表現はトラップ表現と呼ばれます.

(この説明は、現実世界のシステムではまれなトラップ表現を持つことができるプラットフォームにのみ適用されunsigned intます。詳細についてはコメントを参照し、標準の現在の文言につながる別の、おそらくより一般的な原因への参照を参照してください。)

于 2012-08-14T23:50:05.610 に答える
12

(この回答は C 1999 に対応しています。C 2011 については、Jens Gustedt の回答を参照してください。)

C 標準では、初期化されていない自動保存期間のオブジェクトの値を使用することが未定義の動作であるとは述べていません。C 1999 標準の 6.7.8 10 には、「自動保存期間を持つオブジェクトが明示的に初期化されていない場合、その値は不確定です」と記載されています。(この段落では、静的オブジェクトを初期化する方法を定義するため、初期化されていないオブジェクトは自動オブジェクトのみを対象としています。)

3.17.2 では、「不定値」を「未指定の値またはトラップ表現」と定義しています。3.17.3 は、「未規定の値」を「この国際規格がどのような場合に選択される値にも要件を課さない、関連するタイプの有効な値」と定義しています。

したがって、未初期化unsigned int xの値が指定されていない場合は、x -= xゼロを生成する必要があります。それは、それがトラップ表現であるかどうかという問題を残します。6.2.6.1 5 に従って、トラップ値にアクセスすると、未定義の動作が発生します。

一部のタイプのオブジェクトには、浮動小数点数のシグナリング NaN などのトラップ表現がある場合があります。しかし、符号なし整数は特別です。6.2.6.2 に従って、unsigned int の N 値ビットのそれぞれは 2 のべき乗を表し、値ビットの各組み合わせは 0 から 2 N -1 までの値の 1 つを表します。そのため、符号なし整数は、パディング ビット (パリティ ビットなど) の値によってのみトラップ表現を持つことができます。

ターゲット プラットフォームで unsigned int にパディング ビットがない場合、初期化されていない unsigned int はトラップ表現を持つことができず、その値を使用しても未定義の動作が発生することはありません。

于 2012-08-15T00:54:54.910 に答える
11

はい、未定義です。コードがクラッシュする可能性があります。C は、一般的なルールに例外を設ける特別な理由がないため、動作は未定義であると述べています。利点は、未定義の動作の他のすべてのケースと同じ利点です。コンパイラは、これを機能させるために特別なコードを出力する必要はありません。

明らかに、コンパイラは、変数内で「便利」と見なされるガベージ値を単純に使用でき、意図したとおりに機能します...そのアプローチの何が問題なのですか?

なぜそうならないと思いますか?まさにそのアプローチです。コンパイラはそれを機能させる必要はありませんが、失敗させる必要はありません。

于 2012-08-14T23:50:30.957 に答える
10

初期化されていない、またはその他の理由で不確定な値を保持する任意の型の変数の場合、その値を読み取るコードには次のことが適用されます。

  • 変数に自動保存期間があり、そのアドレスが取得されていない場合、コードは常に未定義の動作を呼び出します [1]。
  • それ以外の場合、システムが特定の変数タイプのトラップ表現をサポートしている場合、コードは常に未定義の動作を呼び出します [2]。
  • それ以外の場合、トラップ表現がない場合、変数は未指定の値を取ります。変数が読み取られるたびに、この未指定の値が一貫しているという保証はありません。ただし、トラップ表現ではないことが保証されているため、未定義の動作を呼び出さないことが保証されています [3]。

    この値は、プログラムのクラッシュを引き起こすことなく安全に使用できますが、そのようなコードはトラップ表現を持つシステムに移植できません。


[1]: C11 6.3.2.1:

左辺値が register ストレージ クラスで宣言できた可能性のある自動ストレージ期間のオブジェクトを指定し (そのアドレスが取得されていない)、そのオブジェクトが初期化されていない (初期化子で宣言されておらず、使用前に割り当てが実行されていない) 場合)、動作は未定義です。

[2]: C11 6.2.6.1:

特定のオブジェクト表現は、オブジェクト タイプの値を表す必要はありません。オブジェクトの格納された値がそのような表現を持ち、文字型を持たない左辺値式によって読み取られる場合、動作は未定義です。そのような表現が、文字型を持たない左辺値式によってオブジェクトのすべてまたは一部を変更する副作用によって生成される場合、動作は未定義です.50) このような表現はトラップ表現と呼ばれます.

[3] C11:

3.19.2
不定値
不特定の値またはトラップ表現

3.19.3 unspecified value (
未規定
値) この規格がどのような場合に選択される値にも要件を課さない、関連するタイプの有効な値注記 未規定値は
トラップ表現にはなり得ない。

3.19.4
トラップ表現
オブジェクト型の値を表す必要のないオブジェクト表現

于 2016-11-18T10:35:28.130 に答える
1

多くの回答は、初期化されていないレジスタ アクセスをトラップするプロセッサに焦点を当てていますが、UB を悪用する特別な努力をしないコンパイラを使用すると、そのようなトラップがないプラットフォームでも奇妙な動作が発生する可能性があります。次のコードを検討してください。

volatile uint32_t a,b;
uin16_t moo(uint32_t x, uint16_t y, uint32_t z)
{
  uint16_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z;
  return temp;  
}

ロードとストア以外のすべての命令が 32 ビット レジスタで動作する ARM のようなプラットフォームのコンパイラは、次のような方法でコードを合理的に処理できます。

volatile uint32_t a,b;
// Note: y is known to be 0..65535
// x, y, and z are received in 32-bit registers r0, r1, r2
uin32_t moo(uint32_t x, uint32_t y, uint32_t z)
{
  // Since x is never used past this point, and since the return value
  // will need to be in r0, a compiler could map temp to r0
  uint32_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z & 0xFFFF;
  return temp;  
}

いずれかの揮発性読み取りがゼロ以外の値を生成する場合、r0 には 0 ~ 65535 の範囲の値がロードされます。そうしないと、関数が呼び出されたときに保持されていたもの (つまり、x に渡された値) が生成されます。これは、0..65535 の範囲の値ではない可能性があります。標準には、型が uint16_t であるが値が 0..65535 の範囲外である値の動作を説明する用語がありません。ただし、そのような動作を生成する可能性のあるアクションは UB を呼び出すということを除きます。

于 2016-08-08T16:51:56.553 に答える