web-dev-qa-db-ja.com

「未定義の振る舞い」は本当に*何か*が起こることを許していますか?

編集:この質問は、未定義の振る舞いの(デ)メリットについての議論の場として意図されたものではありませんでしたが、それは一種のことです。いずれにせよ、 未定義の動作を持たない仮想Cコンパイラに関するこのスレッド は、これが重要なトピックであると考える人にとっては追加の関心事かもしれません。


「未定義の振る舞い」の古典的な例は、もちろん「鼻の悪魔」です。これは、CおよびC++標準が許可するものに関係なく、物理的に不可能です。

CおよびC++コミュニティは、未定義の動作の予測不能性と、未定義の動作が行われたときにプログラムが文字通りanythingを行うことをコンパイラが許可するという考えにそのような重点を置く傾向があるため遭遇したとき、私は標準が未定義の振る舞いの振る舞いに全く制限を課さないと仮定していました。

しかし、 C++標準の関連する引用符のようです

[C++14: defns.undefined]:[..]予測できない結果で状況を完全に無視することから、環境に特有の文書化された方法で翻訳またはプログラム実行中に動作することまで(診断メッセージの発行)、翻訳または実行の終了(診断メッセージの発行)。 [..]

これは実際に、可能なオプションの小さなセットを指定します。

  • 状況を無視する-はい、標準はこれが「予測不可能な結果」をもたらすと言っていますが、それはコンパイラと同じではありませんinsertingcode(これは鼻の悪魔の前提条件だと思います)。
  • 環境に特有の文書化された方法で振る舞う-これは実際には比較的良性に聞こえます。 (鼻の悪魔の文書化された事例を聞いたことはありません。)
  • 翻訳または実行の終了-診断もあります。すべてのUBがとてもうまく動作するでしょうか。

ほとんどの場合、コンパイラは未定義の動作を無視することを選択すると想定しています。たとえば、初期化されていないメモリを読み取る場合、一貫した動作を確保するためにコードを挿入することはおそらく最適化されません。未定義の動作( " time travel "など)の見知らぬ人のタイプは2番目のカテゴリに分類されると思いますが、これにはそのような動作を文書化し、「環境の特性」が必要です(鼻の悪魔は地獄のコンピューターによってのみ生成されると思いますか?)。

定義を誤解していますか?これらは、オプションの包括的なリストではなく、未定義の動作を構成する可能性があるものの単なるとして意図されていますか? 「何でも起こり得る」という主張は、単に状況を無視することの予期せぬ副作用を意味したのでしょうか?

EDIT:2つの明確な説明:

  • 私は元の質問からそれが明らかであると思いました、そして、私はそれが大部分の人にそうであったと思います、しかし私はそれをとにかく綴ります:私は「鼻の悪魔」が異様な舌であることを理解します。
  • also実装の最適化を許可する方法を説明しない限り、UBがプラットフォーム固有のコンパイラ最適化を許可することを説明する(他の)回答を書かないでください。 -defined動作は許可されません
93
Kyle Strand

はい、何でも起こります。メモは、単に例を示しています。定義は非常に明確です。

未定義の動作:この国際規格が要件を課さない動作。


混乱の頻発点:

「要件なし」alsoは、実装が[〜#〜] not [〜#〜]であることを意味することを理解する必要があります。非決定的!

実装は、C++標準によって完全な動作を文書化して適切に動作することを完全に許可されています。1 そのため、コンパイラが符号付きオーバーフローでラップアラウンドすると主張する場合、ロジック(正気?)は、その動作に依存することを歓迎することを指示しますそのコンパイラで。他のコンパイラがそれを主張しない場合、同じように振る舞うことを期待しないでください。

1ちなみに、あることを文書化し、別のことを行うことさえ許可されています。それはばかげているだろうし、おそらくあなたはそれをゴミ箱に捨てるだろう-なぜあなたはドキュメントがあなたにあるコンパイラを信頼しますか?-それはC++標準に反していません。

76
Mehrdad

未定義の動作の歴史的な目的の1つは、特定のアクションが異なるプラットフォームで異なるpotentially-useful効果を持つ可能性を許容することでした。たとえば、Cの初期には、

int i=INT_MAX;
i++;
printf("%d",i);

一部のコンパイラは、コードが特定の値を出力することを保証できます(2の補数のマシンの場合、通常はINT_MINになります)。アプリケーションの要件によっては、どちらの動作も役立つ場合があります。動作を未定義のままにしておくと、プログラムの異常終了がオーバーフローの許容可能な結果であるが、一見有効ではあるが間違った出力を生成するアプリケーションは、それを確実にトラップするプラットフォームで実行された場合、オーバーフローチェックを省略できます。オーバーフローが発生した場合の異常終了は受け入れられませんが、算術的に正しくない出力の生成は、オーバーフローがトラップされていないプラットフォームで実行された場合、オーバーフローチェックを省略できます。

しかし、最近、一部のコンパイラ作成者は、標準によって必須ではないコードを誰が最も効率的に削除できるかを競うコンテストに参加したようです。たとえば、...

#include <stdio.h>

int main(void)
{
  int ch = getchar();
  if (ch < 74)
    printf("Hey there!");
  else
    printf("%d",ch*ch*ch*ch*ch);
}

ハイパーモダンコンパイラは、chが74以上の場合、ch*ch*ch*ch*chは未定義の振る舞いをもたらし、結果としてプログラムは「Hey there!」を出力するはずです。入力された文字に関係なく、無条件に。

23
supercat

Nitpicking:標準を引用していません。

これらは、C++標準のドラフトを生成するために使用されるソースです。これらのソースは、C++ワーキンググループ(ISO/IEC JTC1/SC22/WG21)によって公式に採択されない限り、ISOの出版物と見なされるべきではなく、それらから生成されるドキュメントも考慮されるべきではありません。

解釈:ISO/IEC指令パート2によると、注記は 規定 ではありません.

文書の本文に組み込まれている注記と例は、文書の理解または使用を支援するための追加情報を提供するためにのみ使用されます。 要件(「shall」。3.3.1および表H.1を参照)またはドキュメントの使用に不可欠と考えられる情報を含めないでください。指示(必須。表H.1を参照)、推奨事項(「すべき」。3.3.2および表H.2を参照)または許可(「may」。表H.3を参照)。メモは事実の記述として書かれている場合があります。

強調鉱山。これだけでは、「オプションの包括的なリスト」が除外されます。ただし、例を与えることは、「文書の理解を助けるための追加情報」としてカウントされます。

「鼻の悪魔」のミームは、宇宙の膨張がどのように機能するかを説明するために気球を使用するのと同じように、文字通りに取られることを意図していないことに留意してください。それは、何でも許されているときに「未定義の動作」shouldが何をすべきかを議論するのは無謀であることを説明するためです。はい、これは、宇宙空間に実際の輪ゴムがないことを意味します。

15
user5250294

すべてのCおよびC++標準での未定義の動作の定義は、基本的に、標準は何が起こるかについての要件を課さないことです。

はい、それは結果が許可されることを意味します。しかし、必須が発生する特定の結果や、必須が発生しない結果はありません。未定義の動作の特定のインスタンスに応答して特定の動作を一貫して生成するコンパイラとライブラリがあるかどうかは関係ありません-そのような動作は必須ではなく、コンパイラの将来のバグ修正リリースでも変更される可能性があります-コンパイラCおよびC++標準の各バージョンに応じて、完全に正しいままです。

ホストシステムが鼻孔に挿入されたプローブへの接続という形でハードウェアサポートを持っている場合、未定義の動作の発生が望ましくない鼻の影響を引き起こす可能性の範囲内です。

11
Peter

他の回答は一般的な質問に非常によく答えているので、私はあなたのポイントの1つだけに答えると思いましたが、これは対処されていません。

「状況を無視-はい、標準ではこれは「予測不可能な結果」になると言われていますが、コンパイラーがコードを挿入することとは異なります(鼻の悪魔の前提条件だと思います)。 」

鼻の悪魔は、コンパイラが任意のコードを挿入せずに、賢明なコンパイラで非常に合理的に発生すると予想される状況は次のとおりです。

if(!spawn_of_satan)
    printf("Random debug value: %i\n", *x); // oops, null pointer deference
    nasal_angels();
else
    nasal_demons();

コンパイラは、* xがNULLポインター逆参照であることを証明できる場合、一部の最適化の一部として「OK、と言うことができます。したがって、ifのこのブランチでNULLポインターを逆参照したことがわかります。したがって、そのブランチの一部として、私は何でもすることができます。したがって、これに最適化できます。」

if(!spawn_of_satan)
    nasal_demons();
else
    nasal_demons();

「そしてそこから、これに最適化できます。」

nasal_demons();

適切な状況でこの種のものが最適化コンパイラーにとって非常に有用であるが、災害を引き起こす可能性があることがわかります。実際にISこの種のケースを最適化できるように最適化することが重要です。後ほど時間があるときに掘り下げてみようと思います。

編集:最適化に役立つこのようなケースの私の記憶の深さから来た1つの例は、すでに参照されていなくても、(おそらくインライン化されたヘルパー関数で)NULLであることを非常に頻繁にチェックする場所ですそれを変更しました。最適化コンパイラは、あなたがそれを逆参照したことを確認することができます。したがって、すべての「is NULL」チェックを最適化します。なぜなら、それを逆参照して、IS null、 「is NULL」チェックを実行しないことも含めて、同様の引数が他の未定義の動作に適用されると思います。

8
Muzer

まず、ユーザープログラムの動作が未定義であるだけでなく、コンパイラの動作が未定義。同様に、UBは実行時に検出されず、ソースコードのプロパティです。

コンパイラの作成者にとって、「動作は未定義」とは、「この状況を考慮する必要はない」、または「この状況を発生させるソースコードはないと仮定すること」を意味します。コンパイラーは、UBが提示されたときに、意図的または意図せずに何でもできますが、依然として標準に準拠しているため、鼻へのアクセスを許可した場合はそうです...

その場合、プログラムにUBがあるかどうかを常に把握できるとは限りません。例:

_int * ptr = calculateAddress();
int i = *ptr;
_

これがUBになり得るかどうかを知るには、calculateAddress()によって返されるすべての可能な値を知る必要がありますが、これは一般的なケースでは不可能です(「 Halting Problem 」を参照)。コンパイラには2つの選択肢があります。

  • ptrが常に有効なアドレスを持っていると仮定します
  • 特定の動作を保証するランタイムチェックを挿入する

最初のオプションは、プログラムを高速で作成し、プログラマに望ましくない影響を与えないようにします。一方、2番目のオプションは、より安全ですが低速なコードを作成します。

CおよびC++標準では、この選択肢が開いたままであり、ほとんどのコンパイラは最初の選択肢を選択しますが、たとえばJavaは2番目を義務付けています。


動作が実装定義ではなく未定義なのはなぜですか?

Implementation-definedは( N4296 、1.9§2)を意味します:

抽象マシンの特定の側面と操作は、この国際標準では実装定義として説明されています(たとえば、sizeof(int))。これらは、抽象マシンのパラメーターを構成します。各実装には、これらの点での特性と動作を説明する文書が含まれます。そのようなドキュメントは、その実装に対応する抽象マシンのインスタンスを定義するものとします(以下の「対応するインスタンス」と呼びます)。

強調鉱山。言い換えれば、コンパイラーライターは、ソースコードが実装定義の機能を使用する場合、exactlyマシンコードの動作を文書化する必要があります。

ランダムで非ヌルの無効なポインターへの書き込みは、プログラムで実行できる最も予測不可能なことの1つであるため、パフォーマンスを低下させるランタイムチェックも必要になります。
MMUを作成する前に、 destroy hardware 間違ったアドレスに書き込むことで、veryを取得できました。鼻の悪魔に近い;-)

6
alain

動作を未定義のままにする理由の1つは、最適化の際にコンパイラが必要な仮定を行うことを許可することです。

最適化を適用する場合に保持する必要がある条件が存在し、その条件がコード内の未定義の動作に依存している場合、適合プログラムはいずれの未定義の動作にも依存できないため、コンパイラはそれが満たされていると想定します方法。重要なのは、これらの仮定においてコンパイラが一貫している必要がないことです。 (これはnot実装定義の動作の場合)

したがって、コードに以下のような明らかに不自然な例が含まれているとします。

int bar = 0;
int foo = (undefined behavior of some kind);
if (foo) {
   f();
   bar = 1;
}
if (!foo) {
   g();
   bar = 1;
}
assert(1 == bar);

コンパイラーは、最初のブロックで!fooがtrueで、2番目のブロックでfooがtrueであると想定して、コードのチャンク全体を最適化します。論理的にはfooまたは!fooのいずれかがtrueである必要があるため、コードを見ると、コードを実行したらbarが1に等しいと合理的に仮定できます。しかし、コンパイラはそのように最適化されているため、barは1に設定されることはありません。そして、アサーションがfalseになり、プログラムが終了します。これは、fooが未定義の動作.

さて、コンパイラが未定義の動作を見た場合、実際に完全に新しいコードを挿入することは可能ですか?そうすることで、さらに最適化できるようになります。頻繁に起こる可能性はありますか?おそらくそうではありませんが、それを保証することはできませんので、鼻の悪魔が可能だという前提で操作することが唯一の安全なアプローチです。

4
Ray

未定義の動作は、仕様の作成者が予測していなかった状況の結果です。

信号機のアイデアを考えてください。赤は停止、黄色は赤の準備、緑はゴーを意味します。この例では、車を運転する人々が仕様の実装です。

緑と赤の両方が点灯している場合はどうなりますか?止まってから行く?赤が消えて緑になるまで待ちますか?これは、仕様で記述されていないケースであり、その結果、ドライバーが行うことは未定義の動作です。ある人は別のことをします。何が起こるかについての保証はないので、この状況を避けたいと思います。同じことがコードにも当てはまります。

3
Waters

未定義の動作により、コンパイラは場合によってはより高速なコードを生成できます。 ADDが異なる2つの異なるプロセッサアーキテクチャを検討してください。プロセッサAはオーバーフロー時にキャリービットを本質的に破棄し、プロセッサBはエラーを生成します。 (もちろん、プロセッサーCは本質的に鼻腔の悪魔を生成します-鼻水で動くナノボットで余分なエネルギーを放出する最も簡単な方法です...)

標準でエラーを生成する必要がある場合、プロセッサA向けにコンパイルされたすべてのコードは、基本的に追加の命令を追加し、オーバーフローの何らかのチェックを実行し、そうであればエラーを生成します。開発者が小さな数字を追加するだけだとわかっていても、これによりコードが遅くなります。

未定義の動作は、速度の移植性を犠牲にします。 「何でも」発生することを許可することにより、コンパイラーは、決して発生しない状況に対する安全性チェックの作成を回避できます。 (または、あなたは知っている...彼らはかもしれない。)

さらに、プログラマーが特定の環境で未定義の動作が実際に引き起こすことを正確に知っている場合、その知識を活用してパフォーマンスを向上させることができます。

コードがすべてのプラットフォームでまったく同じように動作するようにする場合は、「未定義の動作」が発生しないようにする必要がありますが、これが目標ではない場合があります。

編集:(OPの編集に対応)実装定義された動作には、鼻の悪魔の一貫した生成が必要です。未定義の動作により、鼻の悪魔が散発的に生成されます。

それは、未定義の動作が実装固有の動作よりも優れている点です。特定のシステムで一貫性のない動作を避けるために、追加のコードが必要になる場合があることを考慮してください。これらの場合、未定義の動作により速度が向上します。

1
Allen