未定義行動の背後にある哲学


59

C \ C ++仕様では、コンパイラが独自の方法で実装するための多数の動作が公開されていません。同じことについて常にここで質問され続ける多くの質問があり、それに関するいくつかの素晴らしい投稿があります:

私の質問は、未定義の振る舞いについてではなく、本当に悪いことです。危険と標準からの関連する未定義の動作の引用のほとんどを知っているので、それがどれほど悪いかについての回答を投稿することは控えてください。この質問は、コンパイラ実装のために非常に多くの動作を開いたままにすることの背後にある哲学に関するものです。

パフォーマンスが主な理由であると述べている素晴らしいブログ記事を読みまし。パフォーマンスがそれを許可するための唯一の基準であるのか、それともコンパイラー実装のためにオープンなままにする決定に影響する他の要因があるのか​​疑問に思っていましたか?

特定の未定義の動作がコンパイラが最適化するのに十分な余地を提供する方法について引用する例がある場合は、それらをリストしてください。パフォーマンス以外の他の要因を知っている場合は、十分な詳細を含めて回答を返してください。

質問を理解していないか、回答を裏付ける十分な証拠/情報源がない場合は、広く推測する回答を投稿しないでください。


7
とにかく決定論的なコンピューターを聞いたことがありますか?
ソバ

1
litbの優れた回答Programmers.stackexchange.com/a/99741/192238が示すように、この質問のタイトルと本文は少し不一致のようです。「コンパイラが独自の方法で実装するために開かれた動作」は、通常、実装定義。必ず、実際のUBは、実装の作成者が定義することが許可されているが、より多くの場合より、彼らは気にしない(となど、そのすべてを離れて最適化)
underscore_d

回答:


49

まず、ここでは "C"についてのみ言及していますが、C ++にも同じことが当てはまることに注意してください。

ゲーデルに言及したコメントは部分的に(しかし部分的にのみ)論点でした。

簡単に言えば、C標準の未定義の動作は、標準が定義しようとしていることと定義していないことの境界をに指摘しているだけです。

Godelの定理(2つあります)は、(独自のルールによって)完全で一貫性があることが証明できる数学的システムを定義することは基本的に不可能だと言っています。ルールを作成して完全なものにすることができます(彼が扱ったケースは自然数の「通常の」ルールでした)。そうでなければ、その一貫性を証明することができますが、両方を持つことはできません。

Cのようなものの場合、それは直接当てはまりません。ほとんどの場合、システムの完全性または一貫性の「証明可能性」は、ほとんどの言語設計者にとって高い優先順位ではありません。同時に、はい、おそらく「完全な」システム、つまり完全で一貫性のあるシステムを定義することは不可能であることを知っていることによって、少なくともある程度影響を受けました。そのようなことが不可能であることを知ることは、一歩後退し、少し呼吸し、彼らが定義しようとするものの境界を決定するのを少し簡単にしたかもしれません。

(まだ)慢であると非難される危険性があるので、私はC標準を(部分的に)2つの基本的な考え方に支配されていると特徴付けます。

  1. この言語は、可能な限りさまざまなハードウェアをサポートする必要があります(理想的には、すべての「健全な」ハードウェアを妥当な下限までサポートする必要があります)。
  2. この言語は、指定された環境で可能な限り多種多様なソフトウェアの作成をサポートする必要があります。

最初の意味は、誰かが新しいCPUを定義する場合、設計が少なくともいくつかの単純なガイドラインに合理的に近い限り、そのために、Cの適切で堅実な使用可能な実装を提供できることを意味します-基本的に、 Von Neumannモデルの一般的な順序に従っており、少なくともある程度の合理的な最小メモリ量を提供します。これは、C実装を可能にするのに十分なはずです。「ホストされた」実装(OS上で実行される実装)の場合、ファイルにかなり密接に対応する概念をサポートする必要があり、特定の最小文字セット(91が必要)を持つ文字セットが必要です。

第二は、ハードウェアを直接操作するコードを記述することができなければならないことを意味し、その最終的にありますが、ブートローダー、オペレーティング・システム、など任意のOSなしで実行組み込みソフトウェア、のようなものを書くことができ、いくつかのこの点で限界が、そのほぼすべて実用的なオペレーティングシステム、ブートローダーなどには、アセンブリ言語で記述されたコードが少なくとも少し含まれている可能性があります。同様に、小さな組み込みシステムでも、ホストシステム上のデバイスへのアクセスを許可するために、少なくとも何らかの種類の事前に作成されたライブラリルーチンが含まれている可能性があります。正確な境界を定義することは困難ですが、そのようなコードへの依存を最小限に抑えることを目的としています。

言語での未定義の動作は、主に言語がこれらの機能をサポートするという意図によって決まります。たとえば、この言語を使用すると、任意の整数をポインターに変換し、そのアドレスにあるものにアクセスできます。この規格は、あなたが何をしたときに何が起こるかを言おうとはしません(例えば、いくつかのアドレスからの読み取りでさえ、外部から見える影響を与えることができます)。同時に、それはあなたがそのようなことをすることを妨げることを試みません。なぜなら、あなたはCで書くことができるはずのある種のソフトウェアのために必要だからです。

他の設計要素によって駆動される未定義の動作もあります。たとえば、Cのもう1つの目的は、個別のコンパイルをサポートすることです。これは、たとえば、ほとんどの人が通常のリンカーモデルとして見ているものにほぼ従うリンカーを使用して、ピースを「リンク」できることを意味します。特に、言語のセマンティクスを知らなくても、別々にコンパイルされたモジュールを完全なプログラムに結合することが可能であるべきです。

別のタイプの未定義の動作(CよりもC ++で一般的です)があります。これは、単にコンパイラテクノロジの制限のために存在します。基本的にエラーであり、おそらくコンパイラがエラーとして診断することを知っていること、しかし、コンパイラテクノロジの現在の制限を考えると、すべての状況で診断できるかどうかは疑わしいです。これらの多くは、個別のコンパイルなどのその他の要件によって駆動されるため、主に競合する要件のバランスをとることが問題となります。考えられるすべての問題が診断されるように機能を制限するのではありません。

これらの意図の違いは、CとJavaやMicrosoftのCLIベースのシステムのようなものとの違いのほとんどを促進します。後者は、はるかに限られたハードウェアセットでの作業、またはターゲットとするより具体的なハードウェアをエミュレートするためのソフトウェアを必要とすることに明確に制限されています。また、ハードウェアの直接的な操作を防止することも意図しており、その代わりに、JNIやP / Invoke(およびCのようなコードで記述されたコード)を使用してそのような試みを行う必要があります。

ゴデルの定理に少し戻って、似たようなものを描くことができます。JavaとCLIは「内部的に一貫した」代替案を選択し、Cは「完全な」代替案を選択しました。もちろん、これは非常に大まかな例えです。どちらの場合でも、誰かが内部の一貫性または完全性のいずれかの正式な証明を試みているとは思いません。それにもかかわらず、一般的な概念は、彼らが取った選択にかなり密接に適合します


25
ゲーデルの定理は赤いニシンだと思う。彼らは独自の公理からシステムを証明することを扱っていますが、ここではそうではありません。CをCで指定する必要はありません。完全に指定された言語を持つことはかなり可能です(チューリングマシンを考えてください)。
プーリー

9
すみませんが、ゲーデルの定理を完全に誤解しているのではないかと心配しています。彼らは、一貫した論理システムですべての真のステートメントを証明することは不可能です。計算に関しては、不完全性定理は、どのプログラムでも解決できない問題があると言うことに似ています。問題は、真のステートメント、証明するプログラム、および論理システムへの計算モデルに似ています。未定義の動作とはまったく関係がありません。類推の説明については、scottaaronson.com / blog / ?p = 710を参照してください。
アレックス10ブリンク

5
Von NeumannマシンはC実装には必要ないことに注意してください。ハーバードアーキテクチャ用のC実装を開発することは完全に可能です(非常に難しくはありません)(そして、組み込みシステムでこのような実装が多く見られることは驚くことではありません)
bdonlan

1
残念ながら、現代のCコンパイラの哲学はUBをまったく新しいレベルに引き上げています。特定の形式の未定義動作からのほぼすべてのもっともらしい「自然な」結果を処理するようにプログラムが準備されていて、処理できなかったものが少なくとも認識できる場合(トラップされた整数オーバーフローなど)でも、新しい哲学は好意的ですUBが発生しない限り実行できなかったコードをバイパスし、ほとんどの実装で正しく動作するコードを「より効率的」であるが単なる間違ったコードに変換します。
supercat

20

C根拠は説明します

不特定の動作、未定義の動作、および実装定義の動作という用語は、標準が完全に記述しない、または記述できないプロパティを持つプログラムを作成した結果を分類するために使用されます。この分類を採用する目的は、実装の品質を市場で積極的に活用できるようにする実装の特定の多様性を可能にすることと、標準への適合のキャッシュを削除することなく、特定の人気のある拡張を可能にすることです。標準の付録Fでは、これら3つのカテゴリのいずれかに該当する動作をカタログしています。

不特定の動作により、実装者はプログラムの翻訳にある程度の自由度が与えられます。この許容範囲は、プログラムの翻訳に失敗する限りは拡大しません。

未定義の動作により、実装者は、診断が困難な特定のプログラムエラーをキャッチできないようになります。また、適合可能な言語拡張の可能性のある領域も識別します。実装者は、公式に定義されていない動作の定義を提供することにより、言語を拡張できます。

実装定義の動作により、実装者は適切なアプローチを自由に選択できますが、この選択をユーザーに説明する必要があります。実装定義として指定された動作は、通常、ユーザーが実装定義に基づいて意味のあるコーディング決定を行うことができる動作です。実装者は、実装定義がどれほど広範囲にあるべきかを決定する際に、この基準に留意する必要があります。不特定の動作と同様に、実装定義の動作を含むソースの翻訳に失敗するだけでは適切な応答ではありません。

重要なのは、実装の利点だけでなく、プログラムの利点でもあります。未定義の動作に依存したプログラムはまだすることができます準拠し、それが準拠実装によって受け入れられた場合、。未定義の動作の存在により、プログラムは、不適合にならずに、明示的にそのようにマークされた非ポータブル機能(「未定義の動作」)を使用できます。根拠は次のとおりです。

Cコードは移植できない場合があります。プログラマーに真にポータブルなプログラムを書く機会を与えようと努力しましたが、委員会はプログラマーに移植性のある書き込みを強制し、「高レベルのアセンブラー」としてのCの使用を排除したくありませんでした:コードはCの長所の1つです。この原則が、厳密に準拠するプログラム準拠するプログラム(§1.7)を区別する主な理由です。

1.7では

コンプライアンスの3つの定義は、適合プログラムの数を増やし、単一の実装を使用する適合プログラムとポータブル適合プログラムを区別するために使用されます。

厳密に適合したプログラムは、最大限に移植可能なプログラムの別の用語です。目標は、プログラマーに、偶然にも移植性のない完全に有用なCプログラムを傷つけることなく、移植性の高い強力なCプログラムを作成する格闘の機会を与えることです。したがって、副詞は厳密に。

したがって、GCCで完全に機能するこの小さな汚いプログラムは、まだ適合しています!


15

Cと比較すると、速度の問題は特に問題です。C++が、プリミティブ型の大きな配列を初期化するなど、賢明なことを行うと、Cコードのベンチマークが大量に失われます。そのため、C ++は独自のデータ型を初期化しますが、C型はそのままにします。

他の未定義の動作は現実を反映しています。1つの例は、型より大きいカウントでのビットシフトです。実際には、同じファミリーのハードウェア世代間で異なります。16ビットアプリを使用している場合、まったく同じバイナリを使用しても80286と80386で異なる結果が得られます。したがって、言語標準ではわからないということになっています。

部分式の評価の順序が指定されていないなど、いくつかのことは元のままに保たれています。もともとこれは、コンパイラライターの最適化の向上に役立つと考えられていました。現在、コンパイラーはそれを理解するのに十分ですが、自由を利用する既存のコンパイラーのすべての場所を見つけるコストは高すぎます。


2番目の段落の+1。これは、実装定義の動作として指定するのが面倒なことを示しています。
デビッドソーンリー

3
ビットシフトは、未定義のコンパイラ動作を受け入れ、ハードウェア機能を使用する例にすぎません。カウントがtypeよりも大きい場合、ビットシフトにCの結果を指定するのは簡単ですが、一部のハードウェアでは実装にコストがかかります。
-mattnz

7

一例として、ポインタアクセスはほとんど未定義である必要があり、必ずしもパフォーマンス上の理由だけではありません。たとえば、一部のシステムでは、特定のレジスタにポインターをロードすると、ハードウェア例外が生成されます。SPARCでは、不適切に配置されたメモリオブジェクトにアクセスするとバスエラーが発生しますが、x86では「ちょうど」遅くなります。基礎となるハードウェアが何が起こるかを決定するため、これらの場合の動作を実際に指定することは難しく、C ++は非常に多くのタイプのハードウェアに移植可能です。

もちろん、コンパイラーはアーキテクチャー固有の知識を自由に使用できます。不特定の動作の例では、符号付きの値の右シフトは基礎となるハードウェアに応じて論理的または算術的であり、使用可能なシフト操作を使用し、ソフトウェアエミュレーションを強制しないようにします。

また、コンパイラ作成者の仕事がかなり簡単になると思いますが、今の例を思い出すことはできません。状況を思い出したら追加します。


3
C言語は、アライメント制限のあるシステムで常にバイト単位の読み取りを使用する必要があり、無効なアドレスアクセスに対して明確に定義された動作の例外トラップを提供する必要があるように指定できます。しかし、もちろんこれはすべて(コードサイズ、複雑さ、およびパフォーマンスの面で)信じられないほどコストがかかり、正気で正しいコードに対しては何のメリットもありません。
R ..

6

シンプル:速度と移植性。C ++が無効なポインターを逆参照するときに例外が発生することを保証している場合、組み込みハードウェアに移植できません。C ++が常に初期化されたプリミティブのような他の何かを保証するなら、それはより遅くなるでしょう、そしてC ++の起源の時間では、より遅くは本当に、本当に悪いことでした。


1
え?例外は組み込みハードウェアと何の関係がありますか?
メイソンウィーラー

2
例外は、迅速に応答する必要がある組み込みシステムにとって非常に悪い方法でシステムをロックする可能性があります。誤読がシステムの速度低下よりもはるかに少ない損傷を与える状況があります。
世界エンジニア

1
@Mason:ハードウェアが無効なアクセスをキャッチする必要があるため。Windowsがアクセス違反をスローするのは簡単であり、オペレーティングシステムのない組み込みハードウェアがdie以外の操作を行うのは困難です。
-DeadMG

3
また、すべてのCPUにハードウェアでの無効なアクセスを防ぐためのMMUがあるわけではないことに注意してください。すべてのポインターアクセスをチェックするように言語を要求し始めると、1つなしでCPUでMMUをエミュレートする必要があります。したがって、すべてのメモリーアクセスが非常に高価になります。
ふわふわ

4

Cは、9ビットバイトで浮動小数点ユニットのないマシンで発明されました。バイトが9ビット、ワードが18ビットで、フロートはIEEE 754より前のaritmaticを使用して実装する必要があると仮定した場合


5
Unixのことを考えているのではないかと思います-CはもともとPDP-11で使用されていました。それにもかかわらず、基本的な考え方は立っていると思います。
ジェリーコフィン

@ジェリー-はい、あなたは正しいです-私は老いています!
マーティンベケット

うん-私たちの最高のことが起こる、私は恐れている。
ジェリーコフィン

4

UBの最初の理論的根拠は、コンパイラに最適化の余地を与えることではなかったと思いますが、アーキテクチャが現在よりも多様であったときに、ターゲットに明らかな実装を使用する可能性だけでした(Cが多少馴染みのあるアーキテクチャを持つPDP-11、最初のポートは、あまり馴染みのないHoneywell 635でした-ワードアドレス指定可能、36ビットワード、6または9ビットバイト、18ビットアドレスを使用...少なくとも2つ使用補体)。しかし、重い最適化がターゲットではなかった場合、明らかな実装には、オーバーフローの実行時チェック、レジスタサイズのシフトカウント、複数の値を変更する式のエイリアスの追加は含まれません。

別の考慮事項は、実装の容易さでした。当時のACコンパイラは、1つのプロセスですべてを処理することは不可能であったため(プログラムが大きすぎたため)、複数のプロセスを使用した複数のパスでした。特に複数のCUが関係する場合、重い一貫性チェックを要求することはできませんでした。(そのために、Cコンパイラ以外の別のプログラムlintが使用されました)。


UBの変化する哲学を「プログラマーがプラットフォームで公開されている動作を使用できるようにする」から「コンパイラーがまったく奇抜な動作を実装できるようにする言い訳を見つける」ようになったのはなぜでしょうか。また、新しいコンパイラーで動作するようにコードが変更された後、そのような最適化によってコードサイズがどの程度改善されるのでしょうか。多くの場合、コンパイラにこのような「最適化」を追加することの唯一の効果が、コンパイラがそれを壊さないようにするために、プログラマーに大きくて遅いコードを書くよう強制することである場合、私は驚かないでしょう。
supercat

POVのドリフトです。人々は、プログラムが実行されているマシンにあまり気づかなくなり、移植性に関心を持つようになり、未定義、未指定、実装定義の動作に依存することを避けました。ベンチマークで最良の結果を得るようオプティマイザーに圧力がかかりました。それは、言語の仕様によって残されたすべての寛容さを利用することを意味します。また、インターネット-一度にUsenet、最近ではSE-といった言語弁護士も、コンパイラライターの根底にある根拠と振る舞いについて偏った見方をしがちです。
AProgrammer

1
私が興味を持っているのは、「Cはプログラマが未定義の振る舞いをしないことを前提としている」という効果について私が見た文です。これは歴史的に真実ではありませんでした。正しいステートメントは、「Cは、その動作の自然なプラットフォームの結果に対処する準備ができていない限り、プログラマーが標準で定義されていない動作をトリガーしないと仮定した。Cはシステムプログラミング言語として設計されているため、その目的の大部分プログラマーが言語標準で定義されていないシステム固有のことを行えるようにすることでした;彼らが決してそうしないという考えはばかげています
.- supercat

プログラマーにとって、異なるプラットフォームが本質的に異なることを行う場合に移植性を確保するために余分な努力をするのは良いことですが、コンパイラーの作家は、プログラマーが歴史的に将来のすべてのコンパイラーに共通であると合理的に期待できる振る舞いを排除するとき、すべての時間を無駄にします。整数を考えるin、その結果n < INT_BITSi*(1<<n)オーバーフローではないだろう、私は検討するとi<<=n;より明確にすることi=(unsigned)i << n;。多くのプラットフォームでは、より高速で小さくなりi*=(1<<N);ます。コンパイラがそれを禁止することで得られるものは何ですか?
supercat

標準がUBを呼び出す多くのことに対してトラップを許可することは良いと思いますが(例えば、整数オーバーフロー)、トラップが予測可能な何かをすることを要求しないことには十分な理由がありますが、あらゆる観点から想像できると思いますUBのほとんどの形式が不定の値を生成するか、他の何かを文書化することを絶対に要求されることなく、他の何かを行う権利を留保するという事実を文書化する必要がある場合、標準は改善されます。すべてを「UB」にするコンパイラーは合法ですが、おそらく嫌われる可能性があります
...-supercat

3

初期の古典的なケースの1つは、符号付き整数加算でした。使用中のプロセッサの一部では障害が発生し、他のプロセッサでは値(適切なモジュラー値の可能性が高い)で続行します。どちらの場合も指定すると、好ましくない算術スタイルのマシン用のプログラムは、整数加算と同様の何かのために、条件分岐を含む追加のコードを持たなければならないことを意味します。


整数加算は興味深いケースです。トラップの動作の可能性を超えて、場合によっては役に立つが、他の場合にはランダムなコード実行を引き起こす可能性があるため、整数オーバーフローがラップするように指定されていないという事実に基づいてコンパイラーが推論を行うのが合理的である場合があります。たとえば、int16ビットで符号拡張シフトが高価なコンパイラは(uchar1*uchar2) >> 4、非符号拡張シフトを使用して計算できます。残念ながら、一部のコンパイラは、結果だけでなくオペランドにも推論を拡張します。
supercat

2

私はそれが現実よりも哲学についてではなかったと思います-Cは常にクロスプラットフォーム言語であり、標準はそれを反映しなければならず、標準がリリースされた時点で、多くの異なるハードウェアでの多数の実装。必要な動作を禁止する標準は無視されるか、競合する標準化団体を作成します。


もともと、多くの動作は未定義のままで、異なるシステムが異なることを行う可能性を考慮しており、設定可能な場合と設定できない場合があるハンドラーでハードウェアトラップをトリガーする可能性があります(設定されていない場合、任意に予測不可能な動作を引き起こす可能性があります)。たとえば、負の値の左シフトがトラップではないことを要求すると、そのような動作を行い依存するシステム用に設計されたコードが破損します。要するに、実装者が有用だと思った動作を提供することを妨げないように、それらは未定義のままでした
-supercat

ただし、残念ながら、特定の場合に役立つ何かを実行するプロセッサで実行されていることを知っているコードでさえ、そのような動作を利用できないなど、コンパイラはC標準が(プラットフォームはそうですが)動作を指定せずに、コードに奇妙な世界の書き換えを適用します。
-supercat

1

一部の動作は、合理的な手段では定義できません。削除されたポインターにアクセスするということです。それを検出する唯一の方法は、削除後にポインター値を禁止することです(その値をどこかに記憶し、割り当て関数がそれを返さないようにします)。そのような暗記はやり過ぎになるだけでなく、長時間実行されるプログラムでは、許可されたポインター値が不足する原因になります。


または、すべてのポインタを割り当てweak_ptrて、deleted を取得するポインタへのすべての参照を無効にすることができます。...ちょっと待って、ガベージコレクションに近づいています:/
Matthieu M.

boost::weak_ptrの実装は、この使用パターンの開始点として非常に優れたテンプレートです。weak_ptrs外部で追跡して無効にするのではなく、はの弱いカウントにweak_ptr貢献するだけshared_ptrであり、弱いカウントは基本的にポインター自体への参照カウントです。したがって、shared_ptrすぐに削除せずにを無効化できます。それは完全ではありません(正当な理由がなくても多くの期限切れweak_ptrのsが基礎shared_countを維持することができます)が、少なくとも高速で効率的です。
ふわふわ

0

未定義の動作以外の賢明な選択がほとんどない例を示します。原則として、ポインターは、コンパイラーがアドレスを取得したことがないことをコンパイラーが知ることができるローカル変数の小さな例外を除き、変数を含むメモリーを指すことができます。ただし、最新のCPUで許容可能なパフォーマンスを得るには、コンパイラーは変数値をレジスターにコピーする必要があります。完全にメモリ不足で動作することは、スターターではありません。

これにより、基本的に2つの選択肢が得られます。

1)ポインタが特定の変数のメモリを指している場合に備えて、ポインタを介してアクセスする前に、レジスタからすべてをフラッシュします。次に、ポインタを介して値が変更された場合に備えて、必要なものをすべてレジスタにロードし直します。

2)ポインターが変数のエイリアスを許可する場合と、ポインターが変数をエイリアスしないと仮定することをコンパイラーが許可する場合について、一連のルールを設定します。

Cはオプション2を選択します。パフォーマンスが1になるとひどいためです。しかし、Cルールで禁止されている方法でポインターが変数をエイリアス化するとどうなりますか?コンパイラーが実際に変数をレジスターに格納したかどうかによって影響が異なるため、C標準が特定の結果を確実に保証する方法はありません。


「コンパイラはXが真であるかのように振る舞うことを許可されている」と「Xが真でないプログラムは未定義の動作に従事する」と言うことには意味の違いがありますが、残念ながら区別を明確にしない標準です。エイリアシングの例を含む多くの状況で、前のステートメントは、そうでなければ不可能な多くのコンパイラー最適化を許可します。後者はさらにいくつかの「最適化」を可能にしますが、後者の最適化の多くはプログラマーが望まないものです。
supercat

たとえば、a fooを42に設定し、不正に変更されたポインターを使用しfooて44 に設定するメソッドを呼び出す場合、次の「正当な」書き込みまで、fooそれを読み取ろうとすると正当になります。 yield 42または44、そしてfoo+foo86のような式でも可能ですが、コンパイラが拡張的で遡及的な推論を行うことにより、もっともらしい「自然な」振る舞いがすべて良かったUndefined Behaviorをライセンスに変更することのメリットははるかに少ないと思います無意味なコードを生成します。
supercat

0

歴史的に、未定義の動作には2つの主な目的がありました。

  1. コンパイラの作成者に、発生するはずのない条件を処理するコードの生成を要求することを避けるため。

  2. このような条件を明示的に処理するコードがない場合に、実装には、場合によっては役立つさまざまな種類の「自然な」動作が含まれる可能性を考慮してください。

簡単な例として、一部のハードウェアプラットフォームでは、合計が大きすぎて符号付き整数に収まらない2つの正の符号付き整数を加算しようとすると、特定の負の符号付き整数が生成されます。他の実装では、プロセッサトラップをトリガーします。Cの標準では、どちらの動作でも、標準の動作とは異なる自然な動作を行うプラットフォームのコンパイラーが、正しい動作を実現するために追加のコードを生成する必要があります。さらに悪いことに、「自然な」振る舞いを望むプログラマーは、それを達成するためにさらにコードを追加する必要があります(追加コードは追加コードよりも高価になります)。

残念ながら、一部のコンパイラ作成者は、コンパイラが「定義されていない動作」を引き起こす条件を見つけるために邪魔にならないという哲学を採用しており、そのような状況は決して発生しないと仮定して、それから拡張推論を引き出します。したがって、32ビットを有するシステムにint、与えられたコードのように:

uint32_t foo(uint16_t q, int *p)
{
  if (q > 46340)
    *p++;
  return q*q;
}

C標準では、コンパイラは、qが46341以上の場合、式q * qがに収まりきらないint結果をもたらし、結果として未定義の動作を引き起こし、その結果、コンパイラは発生することはないため、増加*pする必要はありません。呼び出し元のコード*pが計算の結果を破棄する必要があることを示すインジケータとして使用する場合、最適化の効果は、考えられるほとんどすべての方法で整数オーバーフローで実行されるシステムで実用的な結果をもたらすコードを取得することです(トラップはugいですが、少なくとも賢明です)、それを無意味に振る舞うコードに変えました。


-6

効率は通常の言い訳ですが、言い訳が何であれ、未定義の動作は移植性にとってひどい考えです。事実上、未定義の動作は検証されず、述べられていない仮定になります。


7
OPはこれを特定しました:「私の質問は未定義の動作が何であるか、または本当に悪いことではありません。標準からの危険と関連する未定義の動作の引用のほとんどを知っています。 」質問を読んでいないようです。
エティエンヌデマルテル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.