ループのどの時点で整数オーバーフローは未定義の動作になりますか?


86

これは、ここに投稿できないはるかに複雑なコードを含む私の質問を説明するための例です。

#include <stdio.h>
int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        printf("Hello\n");
        a = a + 1000000000;
    }
}

このプログラムにはa、3番目のループでオーバーフローするため、プラットフォームでの未定義の動作が含まれています。

それにより、プログラム全体の動作が未定義になりますか、それともオーバーフローが実際に発生したですか?コンパイラは、潜在的にそれが出て仕事ができるa だろう、それは未定義ループ全体を宣言し、彼らはすべてのオーバーフローが起こる前にもかかわらず、printfを実行する気にすることはできませんので、オーバーフロー?

(CとC ++のタグは異なりますが、両方の言語が異なる場合は回答に興味があるためです。)


7
コンパイラがa(それ自体を計算することを除いて)使用されていないものを解決し、単に削除することができるかa
どうか疑問に思います

12
My Little Optimizer:Undefined Behavior is Magic from CppConyearをお楽しみください。未定義の振る舞いに基づいてコンパイラーが実行できる最適化がすべてです。
TartanLlama 2016年



回答:


108

純粋に理論的な答えに興味がある場合、C ++標準では、未定義の動作を「タイムトラベル」することができます。

[intro.execution]/5: 整形式のプログラムを実行する適合実装は、同じプログラムと同じ入力を持つ抽象マシンの対応するインスタンスの可能な実行の1つと同じ観察可能な動作を生成するものとします。ただし、そのような実行に未定義の操作が含まれている場合、この国際規格は、その入力を使用してそのプログラムを実行する実装に要件を課しません(最初の未定義の操作に先行する操作に関しても)

そのため、プログラムに未定義の動作が含まれている場合、プログラム全体の動作は未定義です。


4
@KeithThompson:しかし、sneeze()関数自体はクラスDemonのどのクラス(鼻の種類はサブクラス)でも定義されていないため、とにかく全体が循環します。
Sebastian Lenartowicz 2016年

1
ただし、printfが返されない可能性があるため、最初の2ラウンドが定義されます。これは、完了するまでUBが存在するかどうかが明確でないためです。stackoverflow.com/questions/23153445/…を
usr

1
:コンパイラは、LinuxカーネルのためのEMIT「NOP」(ブートストラップコードは未定義の動作に依存しているため)にその権利の範囲内、技術的である理由はここにありますblog.regehr.org/archives/761
Crashworks

3
@Crashworksそして、Linuxが移植性のないCで記述され、コンパイルされるのはそのためです(つまり、-fno-strict-aliasingなどの特定のオプションを持つ特定のコンパイラを必要とするCのスーパーセット)
user253751 2016年

3
@usrprintfが返されない場合は定義されていると思いますが、戻る場合printfは、printf呼び出される前に未定義の動作が問題を引き起こす可能性があります。したがって、タイムトラベル。printf("Hello\n");次に、次の行は次のようにコンパイルされますundoPrintf(); launchNuclearMissiles();
user253751 2016年

31

まず、この質問のタイトルを修正しましょう。

未定義の振る舞いは(具体的には)実行の領域ではありません。

未定義の動作は、コンパイル、リンク、ロード、実行のすべてのステップに影響します。

これを固めるためのいくつかの例は、網羅的なセクションがないことを覚えておいてください。

  • コンパイラーは、未定義の振る舞いを含むコードの部分は決して実行されないと想定できるため、それらにつながる実行パスはデッドコードであると想定できます。すべてのCプログラマーが、ChrisLattnerによる未定義の動作について知っておくべきことを参照してください。
  • リンカは、弱い記号(名前で認識される)の複数の定義が存在する場合、単一定義規則のおかげですべての定義が同一であると想定できます。
  • ローダー(ダイナミックライブラリを使用する場合)は同じものを想定できるため、最初に見つかったシンボルを選択します。これは通常LD_PRELOAD、Unixでトリックを使用して通話を傍受するために(ab)使用されます
  • ダングリングポインタを使用すると、実行が失敗する可能性があります(SIGSEV)

これが未定義の振る舞いについてとても怖いことです。どのような正確な振る舞いが発生するかを事前に予測することはほぼ不可能です。この予測は、ツールチェーン、基盤となるOSなどの更新のたびに再検討する必要があります...


Michael Spencer(LLVM開発者)によるこのビデオを見ることをお勧めします:CppCon 2016:My Little Optimizer:Undefined Behavior isMagic


3
これが私を心配していることです。私の実際のコードでは、それは複雑だが、私は可能性がある場所、それが常にオーバーフローする場合があります。そして、私はそれについてはあまり気にしませんが、「正しい」コードもこれによって影響を受けるのではないかと心配しています。明らかに私はそれを修正する必要がありますが、修正には理解が必要です:)
jcoder 2016年

8
@jcoder:ここに重要なエスケープが1つあります。コンパイラは、入力データを推測することはできません。未定義の振る舞いが発生しない入力が少なくとも1つある限り、コンパイラーは、この特定の入力が引き続き正しい出力を生成することを確認する必要があります。危険な最適化に関するすべての恐ろしい話は、避けられないUBにのみ当てはまります。実際には、argcループカウントとして使用した場合、ケースargc=1はUBを生成せず、コンパイラはそれを処理するように強制されます。
MSalters 2016年

@jcoder:この場合、これはデッドコードではありません。ただし、コンパイラーは、iそれを複数N回インクリメントできないため、その値が制限されていると推測できるほど賢い場合があります。
Matthieu M.

4
@jcoder:もしは、f(good);いくつかのことをXを行い、f(bad);単に呼び出すことが未定義の動作、プログラム呼び出しf(good);Xを行うことが保証されますが、f(good); f(bad);X.行うことを保証するものではありません

4
@Hurkylさらに興味深いことに、コードがのif(foo) f(good); else f(bad);場合、スマートコンパイラは比較を破棄して無条件で生成しfoo(good)ます。
ジョン・ドヴォルザーク

28

16ビットintを対象とする積極的に最適化されたCまたはC ++コンパイラは、型に追加する際の動作が未定義であることを認識します。1000000000int

それは望んで何もやって、標準で許可されている可能性が残さ、プログラム全体の削除が含まれるがint main(){}

しかし、より大きなintsはどうですか?私は(と私は任意の手段によって、CおよびC ++コンパイラの設計の専門家ではないよ)まだこれを行い、コンパイラのか分からないが、私はと想像いつか32ビットのターゲットコンパイラint以上がループすることを把握します無限である(i変更しない)と、そうaは最終的にオーバーフロー。したがって、もう一度、出力をに最適化できますint main(){}。ここで私が言いたいのは、コンパイラの最適化が次第に積極的になるにつれて、ますます多くの未定義の動作構造が予期しない方法で現れているということです。

ループ本体の標準出力に書き込んでいるため、ループが無限であるという事実自体は未定義ではありません。


3
未定義の振る舞いが現れる前でさえ、標準によってそれが望むことをすることは許可されていますか?これはどこに述べられていますか?
jimifiki 2016年

4
なぜ16ビット?OPは32ビットの符号付きオーバーフローを探していると思います。
4386427 2016年

8
@jimifiki標準で。C ++ 14(N4140)1.3.24「未定義の動作=この国際規格が要件を課していない動作。」加えて、詳しく説明する長いメモ。しかし、重要なのは、定義されていないのは「ステートメント」の動作ではなく、プログラムの動作であるということです。つまり、UBが標準のルールによって(またはルールがないことによって)トリガーされる限り、標準はプログラム全体の適用を停止しますしたがって、プログラムのどの部分でも、好きなように動作できます。
–Angewは2016

5
最初のステートメントは間違っています。場合int16ビットであり、さらには、で行われるlong(リテラルオペランドがタイプ持っているのでlong、それが明確に定義されています)、その後に実装定義変換バックによって変換することint
R .. GitHub STOP HELPING ICE

2
@usrの動作はprintf、常に返されるように標準で定義されています
MM

11

技術的には、C ++標準では、プログラムに未定義の動作が含まれている場合、コンパイル時でもプログラム全体の動作です。(プログラムが実行される前)であっても、プログラムは未定義です。

実際には、コンパイラーは(最適化の一部として)オーバーフローが発生しないと想定する可能性があるため、少なくともループの3回目の反復(32ビットマシンを想定)でのプログラムの動作は未定義になりますが、 3回目の反復の前に正しい結果が得られる可能性があります。ただし、プログラム全体の動作は技術的に未定義であるため、プログラムが完全に誤った出力(出力なしを含む)を生成したり、実行中の任意の時点でクラッシュしたり、コンパイルに失敗したりすることを妨げるものは何もありません(未定義の動作はコンパイル時)。

未定義の振る舞いは、コードが何をしなければならないかについての特定の仮定を排除するため、コンパイラーに最適化する余地を提供します。そうすることで、未定義の動作を含む仮定に依存するプログラムは、期待どおりに動作することが保証されません。そのため、C ++標準で未定義と見なされる特定の動作に依存しないでください。


UBパーツがif(false) {}スコープ内にある場合はどうなりますか?コンパイラがすべてのブランチにロジックの明確に定義された部分が含まれていると想定し、誤った想定で動作しているため、プログラム全体に悪影響を及ぼしますか?
mlvljr 2016年

1
この規格は未定義の振る舞いに何の要件も課していないので、理論的にはそうです、それはプログラム全体を害します。ただし、実際には、最適化コンパイラはデッドコードを削除するだけなので、実行には影響しない可能性があります。ただし、この動作に依存するべきではありません。
bwDraco 2016年

知っておきたい、
ありがとう

9

@TartanLlamaが適切に表現しているように未定義の動作が「タイムトラベル」できる理由を理解するために、「as-if」ルールを見てみましょう。

1.9プログラムの実行

1この国際規格のセマンティック記述は、パラメータ化された非決定論的抽象マシンを定義します。この国際規格は、適合実装の構造に要件を課していません。特に、抽象マシンの構造をコピーしたりエミュレートしたりする必要はありません。むしろ、以下で説明するように、抽象マシンの観察可能な動作を(のみ)エミュレートするには、準拠する実装が必要です。

これにより、プログラムを入力と出力を備えた「ブラックボックス」と見なすことができます。入力は、ユーザー入力、ファイル、および他の多くのものである可能性があります。出力は、標準で言及されている「観察可能な動作」です。

この規格は、入力と出力の間のマッピングのみを定義し、他には何も定義していません。これは「ブラックボックスの例」を説明することによって行われますが、同じマッピングを持つ他のブラックボックスも同様に有効であると明示的に述べています。これは、ブラックボックスの内容が無関係であることを意味します。

このことを念頭に置いて、未定義の動作が特定の瞬間に発生すると言っても意味がありません。ブラックボックスのサンプル実装では、いつどこで発生するかを言うことができますが、実際にはブラックボックスはまったく異なるものである可能性があるため、どこでいつ発生するかはわかりません。理論的には、コンパイラーは、たとえば、考えられるすべての入力を列挙し、結果の出力を事前に計算することを決定できます。その場合、コンパイル中に未定義の動作が発生します。

未定義の動作は、入力と出力の間にマッピングが存在しないことです。プログラムは、一部の入力に対して未定義の動作を持つことができますが、他の入力に対しては定義された動作を持つことができます。その場合、入力と出力の間のマッピングは単純に不完全です。出力へのマッピングが存在しない入力があります。
問題のプログラムは、どの入力に対しても未定義の動作をしているため、マッピングは空です。


6

int32ビットであると仮定すると、未定義の動作は3回目の反復で発生します。したがって、たとえば、ループが条件付きでのみ到達可能である場合、または3番目の反復の前に条件付きで終了できる場合、3番目の反復に実際に到達しない限り、未定義の動作はありません。ただし、未定義の動作が発生した場合、プログラムのすべての出力は未定義になります。これには、未定義の動作の呼び出しに関連する「過去」の出力も含まれます。たとえば、あなたの場合、これは、出力に3つの「Hello」メッセージが表示される保証がないことを意味します。


6

TartanLlamaの答えは正しいです。未定義の動作は、コンパイル時であってもいつでも発生する可能性があります。これはばかげているように見えるかもしれませんが、コンパイラーが必要なことを実行できるようにするための重要な機能です。コンパイラになるのは必ずしも簡単ではありません。毎回、仕様の内容を正確に実行する必要があります。ただし、特定の動作が発生していることを証明するのが非常に難しい場合があります。停止性問題を覚えているなら、特定の入力が与えられたときにそれが完了するか無限ループに入るかを証明できないソフトウェアを開発するのはかなり簡単です。

コンパイラを悲観的にし、次の命令が問題のようなこれらの停止問題の1つになるのではないかと恐れて絶えずコンパイルすることもできますが、それは合理的ではありません。代わりに、コンパイラにパスを与えます。これらの「未定義の振る舞い」のトピックについては、責任を負いません。未定義の振る舞いは、非常に微妙に悪意のあるすべての振る舞いで構成されているため、本当に厄介で悪意のある停止問題などからそれらを分離するのに問題があります。

ソースを失ったことは認めますが、投稿するのが大好きな例があります。言い換える必要があります。これは、MySQLの特定のバージョンからのものでした。MySQLでは、ユーザー提供のデータで満たされた循環バッファーがありました。もちろん、彼らはデータがバッファをオーバーフローしないことを確認したかったので、彼らはチェックをしました:

if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }

それは十分に正気に見えます。ただし、numberOfNewCharsが本当に大きく、オーバーフローした場合はどうなりますか?次に、ラップアラウンドして、よりも小さいポインタになるendOfBufferPtrため、オーバーフローロジックが呼び出されることはありません。そこで彼らは、その前に2番目のチェックを追加しました。

if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }

バッファオーバーフローエラーを処理したようですね。ただし、このバッファが特定のバージョンのDebianでオーバーフローしたというバグが提出されました。注意深く調査したところ、このバージョンのDebianは、特に最先端のバージョンのgccを最初に使用したことがわかりました。このバージョンのgccでは、ポインターのオーバーフローは未定義の動作であるため、コンパイラーはcurrentPtr + numberOfNewCharsがcurrentPtrよりも小さいポインターになることは決してないことを認識しました。gccがチェック全体を最適化するにはこれで十分であり、チェックするコードを記述したにもかかわらず、突然バッファオーバーフローから保護されなくなりました。

これはスペックの振る舞いでした。すべてが合法でした(私が聞いたところによると、gccはこの変更を次のバージョンでロールバックしました)。これは私が直感的な動作と考えるものではありませんが、想像力を少し伸ばせば、この状況のわずかな変化がコンパイラの停止問題になる可能性があることを簡単に理解できます。このため、スペック作成者はそれを「未定義の振る舞い」にし、コンパイラーは絶対に好きなことを何でもできると述べました。


範囲が「int」を超える型で符号付き演算が実行されるように動作することがある、特に驚くべきコンパイラは考慮しません。特に、x86で単純なコード生成を実行する場合でも、中間を切り捨てるよりも効率的である場合があることを考慮します。結果。さらに驚くべきことは、オーバーフローが他の計算に影響を与える場合です。これは、コードが2つのuint16_t値の積をuint32_tに格納している場合でも、gccで発生する可能性があります。これは、非サニタイズビルドで驚くべき動作をするもっともらしい理由がないはずです。
スーパーキャット2016年

もちろん、if(numberOfNewChars > endOfBufferPtr - currentPtr)numberOfNewCharsが負になることはなく、currentPtrが常にバッファ内のどこかを指している場合、正しいチェックは次のようになります。ばかげた「ラップアラウンド」チェックも必要ありません。(あなたが提供したコードには、循環バッファーで機能する
見込み

@ Random832私は1トンを省略しました。より大きなコンテキストを引用しようとしましたが、ソースを失ったため、コンテキストを言い換えるとさらに問題が発生することがわかったので、省略しました。適切に引用できるように、その爆破されたバグレポートを見つける必要があります。これは、コードを一方向に記述し、まったく異なる方法でコンパイルする方法を示す強力な例です。
コートアンモン2016年

これは、未定義の振る舞いに関する私の最大の問題です。正しいコードを記述できない場合があり、コンパイラがそれを検出すると、デフォルトでは、未定義の動作がトリガーされたことを通知しません。この場合、ユーザーは単に算術演算(ポインターかどうか)を実行したいだけで、安全なコードを作成するためのすべてのハードワークが取り消されました。少なくとも、コードのセクションに注釈を付ける方法があるはずです。ここでは、派手な最適化はありません。C / C ++は非常に多くの重要な領域で使用されているため、この危険な状況を継続して最適化を支持することはできません
John McGrath

4

理論的な答えを超えて、実際的な観察は、コンパイラがループ内で行われる作業の量を減らすために、長い間ループにさまざまな変換を適用してきたということです。たとえば、次のようになります。

for (int i=0; i<n; i++)
  foo[i] = i*scale;

コンパイラはそれを次のように変換するかもしれません:

int temp = 0;
for (int i=0; i<n; i++)
{
  foo[i] = temp;
  temp+=scale;
}

したがって、ループの反復ごとに乗算を節約できます。コンパイラーがさまざまな程度の攻撃性で適応した追加の最適化形式は、それを次のように変換します。

if (n > 0)
{
  int temp1 = n*scale;
  int *temp2 = foo;
  do
  {
    temp1 -= scale;
    *temp2++ = temp1;
  } while(temp1);
}

オーバーフロー時にサイレントラップアラウンドを実行するマシンでも、n未満の数値があると誤動作する可能性があり、スケールを掛けると0になります。また、スケールがメモリから複数回読み取られると、エンドレスループになる可能性があります。予期せず値を変更しました(「スケール」がUBを呼び出さずにループの途中で変更される可能性がある場合、コンパイラーは最適化を実行できません)。

このような最適化のほとんどは、2つの短い符号なし型を乗算してINT_MAX + 1とUINT_MAXの間にある値を生成する場合には問題ありませんが、gccには、ループ内でこのような乗算を行うとループが早期終了する場合があります。 。生成されたコードの比較命令に起因するこのような動作には気づいていませんが、コンパイラーがオーバーフローを使用して、ループが最大4回以下で実行できると推測する場合に観察できます。推論によってループの上限が無視されたとしても、一部の入力でUBが発生し、他の入力では発生しない場合、デフォルトでは警告は生成されません。


4

未定義の動作は、定義上、灰色の領域です。何をするか、何をしないかを予測することはできません。それが「未定義の振る舞い」の意味です。です。

太古の昔から、プログラマーは常に未定義の状況から定義の残骸を救おうとしました。彼らはいくつかのコード、彼らが主張しようとするので、彼らは本当に、使用したいが、未定義であることが判明しているんだ:「私はそれが未定義だけど、確かにそれは、最悪で、このまたはこれを行いますが、それは決してしないだろうということを。」そして、時にはこれらの議論は多かれ少なかれ正しいです-しかし、しばしば、それらは間違っています。そして、コンパイラーがますます賢くなるにつれて(または、一部の人々は、sneakierとsneakierと言うかもしれません)、質問の境界は変化し続けます。

したがって、実際には、動作が保証され、長期間動作し続けるコードを記述したい場合は、唯一の選択肢があります。未定義の動作を絶対に避けてください。確かに、あなたがそれに手を出すと、それはあなたを悩ませるために戻ってきます。


それでも、ここに問題があります...コンパイラは未定義の動作を使用して最適化できますが、一般的には教えてくれません。それで、Xを絶対に避けなければならないこの素晴らしいツールがある場合、コンパイラが警告を出して修正できないのはなぜですか?
ジェイソンS

1

あなたの例が考慮していないことの1つは、最適化です。 aはループに設定されますが、使用されることはなく、オプティマイザーがこれを解決できます。そのため、オプティマイザーが破棄することは正当ですaオプティマイザーが完全であり、その場合、未定義の動作はすべて、ブージュムの犠牲者のように消えます。

ただし、最適化が定義されていないため、もちろんこれ自体は定義されていません。:)


1
動作が未定義であるかどうかを判断するときに最適化を検討する理由はありません。
キーストンプソン

2
プログラムが想定どおりに動作するという事実は、未定義の動作が「消滅」することを意味するものではありません。動作はまだ定義されておらず、あなたは単に運に頼っています。プログラムの動作がコンパイラオプションに基づいて変化する可能性があるという事実は、動作が未定義であることを示す強力な指標です。
ジョーダンメロ

@JordanMelo以前の回答の多くが最適化について説明していたので(そしてOPがそれについて具体的に尋ねたので)、以前の回答ではカバーされていなかった最適化の機能について言及しました。また、最適化によってそれ削除される可能性があるとしても、特定の方法で機能するための最適化への依存は未定義であることも指摘しました。私は確かにそれをお勧めしません!:)
グラハム

@KeithThompson確かに、OPは特に、最適化と、プラットフォームで見られる未定義の動作への影響について質問しました。最適化によっては、その特定の動作が消える可能性があります。しかし、私の答えで言ったように、未定義はそうではありません。
グラハム

0

この質問はCとC ++の二重タグが付いているので、両方に対処しようとします。ここでは、CとC ++は異なるアプローチを取ります。

Cでは、実装は、プログラム全体を未定義の動作があるかのように扱うために、未定義の動作が呼び出されることを証明できなければなりません。OPの例では、コンパイラがそれを証明するのは簡単なように思われるため、プログラム全体が未定義であるかのようになります。

これは、欠陥レポート109から確認できます。

ただし、C標準が「未定義の値」(単なる作成には完全に「未定義の動作」が含まれない)の別個の存在を認識している場合、コンパイラテストを行う人は、次のようなテストケースを作成できます。 (またはおそらく要求する)適合実装は、少なくとも、「失敗」することなくこのコードをコンパイルする(そしておそらく実行を許可する)必要があります。

int array1[5];
int array2[5];
int *p1 = &array1[0];
int *p2 = &array2[0];

int foo()
{
int i;
i = (p1 > p2); /* Must this be "successfully translated"? */
1/0; /* Must this be "successfully translated"? */
return 0;
}

つまり、最終的な質問は次のとおりです。上記のコードを「正常に翻訳」する必要がありますか(それが意味するものは何でも)?(5.1.1.3節に添付されている脚注を参照してください。)

応答は次のとおりです。

C標準では、「未定義の値」ではなく「不確定に評価される」という用語を使用しています。不確定な値のオブジェクトを使用すると、未定義の動作が発生します。5.1.1.3節の脚注は、有効なプログラムが正しく翻訳されている限り、実装は任意の数の診断を自由に生成できることを指摘しています。 評価によって未定義の動作が発生する式が、定数式が必要なコンテキストで表示される場合、含まれているプログラムは厳密には準拠していません。さらに、特定のプログラムを実行するたびに未定義の動作が発生する場合、特定のプログラムは厳密には準拠していません。 準拠する実装は、厳密に準拠するプログラムの変換に失敗してはなりません。そのプログラムを実行すると、未定義の動作が発生する可能性があるからです。fooが呼び出されることはない可能性があるため、指定された例は、準拠する実装によって正常に変換される必要があります。

C ++では、アプローチはよりリラックスしているように見え、実装が静的に証明できるかどうかに関係なく、プログラムの動作が未定義であることを示唆します。

我々は持っている[intro.abstrac] P5言いました:

整形式のプログラムを実行する適合実装は、同じプログラムと同じ入力を持つ抽象マシンの対応するインスタンスの可能な実行の1つと同じ観察可能な動作を生成するものとします。ただし、そのような実行に未定義の操作が含まれている場合、このドキュメントでは、その入力を使用してそのプログラムを実行する実装に要件はありません(最初の未定義の操作に先行する操作に関しても)。


関数の実行がUBを呼び出すという事実は、特定の入力が与えられたときにプログラムの少なくとも1つの可能な実行がUBを呼び出す場合にのみ、プログラムの動作に影響を与える可能性があります。関数を呼び出すとUBが呼び出されるという事実は、関数の呼び出しを許可しない入力が与えられたときに、プログラムが定義された動作をすることを妨げません。
スーパーキャット2018

@supercat少なくともCについて私たちが言っているのはそれだと思います。
Shafik Yaghmour 2018

「そのような実行」というフレーズは、プログラムが特定の特定の入力で実行できる方法を指しているため、C ++に関する引用テキストにも同じことが当てはまると思います。特定の入力で関数が実行されなかった場合、引用されたテキストには、そのような関数のいずれかがUBになるとは言えません。
スーパーキャット2018

-2

一番の答えは間違った(しかし一般的な)誤解です:

未定義の動作は実行時のプロパティです*。それはできません「タイムトラベル」!

特定の操作は(標準で)副作用があると定義されており、最適化することはできません。I / Oを実行する操作、またはvolatile変数にアクセスする操作は、このカテゴリに分類されます。

ただし、注意点があります。UBは、前の操作を元に戻す動作を含む、任意の動作である可能性があります。これは、場合によっては、以前のコードを最適化することと同様の結果をもたらす可能性があります。

実際、これはトップアンサーの引用と一致しています(私の強調):

整形式のプログラムを実行する適合実装は、同じプログラムと同じ入力を持つ抽象マシンの対応するインスタンスの可能な実行の1つと同じ観察可能な動作を生成するものとします。
ただし、そのような実行に未定義の操作が含まれている場合、この国際標準では、その入力を使用してそのプログラムを実行する実装に要件はありません(最初の未定義の操作に先行する操作に関しても)。

はい、この引用「最初の未定義の操作に先行する操作に関してさえも」言っていませんが、これは特にコンパイルされているだけでなく、実行されているコードに関するものであることに注意してください。
結局のところ、実際に到達していない未定義の動作は何もしません。UBを含む行に実際に到達するには、その前のコードを最初に実行する必要があります。

そうです、UBが実行されると、以前の操作の影響は未定義になります。しかし、それが起こるまで、プログラムの実行は明確に定義されています。

ただし、これが発生するプログラムのすべての実行は、以前の操作を実行した後、その効果を元に戻すプログラムを含め、同等のプログラムに最適化できることに注意してください。その結果、先行するコードは、その効果が取り消されるのと同等になる場合はいつでも最適化される可能性があります。そうでなければ、それはできません。例については、以下を参照してください。

*注:これは、コンパイル時に発生するUBと矛盾しません。コンパイラがUBコード常にすべての入力に対して実行されることを実際に証明できる場合、UBはコンパイル時間まで延長できます。ただし、これには、以前のすべてのコードが最終的にを返すことを知っている必要があります。これは強力な要件です。繰り返しますが、例/説明については以下を参照してください。


次のコードがあること、このコンクリート、メモしておく必要があります印刷fooし、それを次のいずれかの未定義の動作に関係なく、あなたの入力を待ちます。

printf("foo");
getchar();
*(char*)1 = 1;

ただし、fooUBが発生した後も画面に表示される保証はないこと、または入力した文字が入力バッファーに存在しないことにも注意してください。これらの操作は両方とも「元に戻す」ことができ、UBの「タイムトラベル」と同様の効果があります。

場合はgetchar()行がありませんでした、それがラインが離れて最適化されるために法的なことだけならばならばそれは次のようになり区別できない出力からfoo、その後、それを「非やって」。

この2つを区別できないかどうかは、実装(つまり、コンパイラと標準ライブラリ)に完全に依存します。たとえば、別のプログラムが出力を読み取るのを待っている間、ここでスレッドをprintf ブロックできますか?それともすぐに戻りますか?

  • ここでブロックできる場合、別のプログラムがその完全な出力の読み取りを拒否する可能性があり、その結果、UBが実際に発生することはありません。

  • ここですぐに戻ることができれば、戻らなければならないことがわかります。したがって、最適化することは、実行してからその効果を元に戻すこととまったく区別がつきません。

もちろん、コンパイラは特定のバージョンprintfので許容される動作を認識しているため、それに応じて最適化でき、その結果printf、最適化される場合とされない場合があります。しかし、繰り返しになりますが、これはUBが以前の操作を行っていないことと区別がつかないということであり、以前のコードがUBのために「ポイズニング」されているということではありません


1
あなたは完全に標準を誤解しています。プログラム実行時の動作が未定義であると表示されます。限目。この答えは100%間違っています。標準は非常に明確です-実行の素朴なフローの任意の時点でUBを生成する入力でプログラムを実行することは定義されていません。
デビッドシュワルツ

@DavidSchwartz:論理的な結論にあなたの解釈に従うならば、あなたはそれが論理的に意味をなさないことに気付くはずです。入力は、プログラムの開始時に完全に決定されるものではありません。任意の行でのプログラムへの入力(その単なる存在でさえ)は、その行までのプログラムのすべての副作用に依存することができます。したがって、プログラムはUBラインの前に発生する副作用の発生を回避できません。これは、環境との相互作用が必要であり、そもそもUBラインに到達するかどうかに影響するためです。
user541686 2016年

3
それは問題ではありません。本当に。繰り返しますが、あなたは想像力に欠けています。たとえば、コンパイラが、準拠コードが違いを認識できないことを認識できる場合、UBであるコードを移動して、UBである部分が、単純に「先行」すると予想される出力の前に実行されるようにすることができます。
デビッドシュワルツ

2
@Mehrdad:おそらく、物事を言うより良い方法は、UBは、行動を定義する現実の世界で何かが起こった可能性がある最後のポイントを過ぎてタイムトラベルすることはできないと言うことでしょう。実装が入力バッファーを調べて、getchar()への次の1000回の呼び出しでブロックする方法がないと判断でき、1000回目の呼び出しの後にUBが発生することも判断できる場合は、次のいずれも実行する必要はありません。呼び出し。しかし、実装は、実行はgetchar()が前の出力が持っていたまでは...合格しないことを指定した場合
supercat

2
... 300ボーの端末に配信され、その前にcontrol-Cが発生すると、その前のバッファに他の文字があったとしてもgetchar()が信号を生成するため、このような実装はできませんでした。 getchar()の前の最後の出力を超えてUBを移動します。トリッキーなのは、どのような場合にコンパイラがプログラマを通過することが期待されるべきかを知ることです。ライブラリの実装が標準で義務付けられているものを超えて提供する可能性のある動作保証。
スーパーキャット2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.