web-dev-qa-db-ja.com

未定義の動作を含むソースコードがコンパイラをクラッシュさせることは合法ですか?

未定義の動作を引き起こす、不完全に記述されたC++ソースコードをコンパイルしようとすると、(何もかもが発生する可能性があります)。

C++言語仕様が「適合」コンパイラーで許容できると見なす観点から、このシナリオの「何か」には、コンパイラーのクラッシュ(またはパスワードの盗用、またはコンパイル時の誤動作やエラーアウト)が含まれます。未定義の動作の範囲は、結果として得られる実行可能ファイルの実行時に何が起こる可能性があるかに限定されますか?

84
Jeremy Friesner

未定義の動作の規範的な定義は次のとおりです。

[defns.undefined]

この国際規格が要件を課していない動作

[注:この国際標準が動作の明示的な定義を省略した場合、またはプログラムが誤った構成体または誤ったデータを使用した場合、未定義の動作が予想される場合があります。許容される未定義の動作は、予測できない結果で完全に状況を無視することから、環境に特徴的な文書化された方法で(診断メッセージの発行の有無にかかわらず)変換またはプログラムの実行中に動作すること、翻訳または実行を終了すること(発行とともに)に及ぶ診断メッセージの)。エラーのあるプログラム構造の多くは、未定義の動作を引き起こしません。診断する必要があります。定数式の評価は、未定義として明示的に指定された動作を示すことはありません。 —メモを終了]

メモ自体は規範的ではありませんが、実装が示すことが知られている動作の範囲を説明しています。そのため、そのメモによると、コンパイラのクラッシュ(翻訳が突然終了する)は合法です。しかし、実際には、規範的なテキストが言うように、標準は実行または翻訳のいずれにも境界を設定しません。実装がパスワードを盗んだとしても、それは規格に定められた契約の違反ではありません。

NULL-derefやゼロ除算など、私たちが通常心配するほとんどの種類のUBはruntime UBです。ランタイムUBの原因となる関数のコンパイル実行された場合は、コンパイラーをクラッシュさせてはなりません。関数(および関数のパス)を証明できない場合間違いなくwillプログラムによって実行されます。

(2番目の考え:コンパイル時にテンプレート/ constexprが必要とする評価を考慮していない可能性があります。その結果のUBは、結果の関数が呼び出されない場合でも、変換中に任意の奇妙さを引き起こす可能性があります。)

翻訳中の動作 ISO C++の引用の一部 @ StoryTellerの回答 は、ISO C標準で使用される言語に似ています。 Cには、コンパイル時にテンプレートまたはconstexpr必須評価が含まれていません。

しかし、おもしろい事実:ISO Cは、翻訳が終了した場合、診断メッセージを伴う必要があると注記で述べています。または、「文書化された方法で翻訳中に動作する」。 「状況を完全に無視する」というのは、翻訳をやめるという意味では読めないと思います。


古い回答、私が翻訳時UBについて学ぶ前に書かれました。ただし、ランタイムUBにも当てはまるので、まだ有用である可能性があります。


コンパイル時にhappensというUBのようなものはありません。特定の実行パスに沿ってvisibleをコンパイラに渡すことができますが、C++の用語では、実行が関数を通じてその実行パスに到達するまでhappenedしません。

コンパイルすら不可能にするプログラムの欠陥はUBではなく、構文エラーです。このようなプログラムは、C++の用語では「整形式ではありません」(標準が正しい場合)。プログラムは整形式ですが、UBを含みます。 未定義の動作と不正な形式の違い、診断メッセージは不要

誤解しない限り、ISO C++ 必須このプログラムは正しくコンパイルして実行する必要があります。実行がゼロによる除算に達することは決してないからです。 (実際には( Godbolt )、優れたコンパイラーは動作する実行可能ファイルを作成するだけです。gcc/ clangは_x / 0_について警告しますが、最適化した場合でもこれは警告しません。 low ISO C++では、実装の品質を維持できます。したがって、gcc/clangをチェックすることは、プログラムを正しく記述したことを確認する以外に、ほとんど有用なテストではありません。)

_int cause_UB() {
    int x=0;
    return 1 / x;      // UB if ever reached.
 // Note I'm avoiding  x/0  in case that counts as translation time UB.
 // UB still obvious when optimizing across statements, though.
}

int main(){
    if (0)
        cause_UB();
}
_

これの使用例には、Cプリプロセッサ、またはconstexpr変数とそれらの変数の分岐が含まれる可能性があり、定数の選択では到達できない一部のパスで意味がないことにつながります。

コンパイル時に表示されるUBの原因となる実行パスは、決して実行されないと想定できます。 x86のコンパイラは、cause_UB()の定義として_ud2_(不正な命令例外の原因)を発行する可能性があります。または、関数内で、if()の片側がprovable UBにつながる場合、ブランチを削除できます。

しかし、コンパイラはまだすべてをコンパイルする必要がありますelseは正気で正しい方法で。 do n'tが遭遇する(または遭遇することが証明できない)すべてのパスは、C++抽象マシンが実行していた場合と同じように実行されるasmにコンパイルする必要があります。


mainの無条件のコンパイル時に表示されるUBはこのルールの例外であると主張できます。または、mainで実行が開始されるコンパイル時の可能性実際には、保証されたUBに到達します。

正当なコンパイラの動作には、爆発する手榴弾を生成することも含まれると私はまだ主張しますif run。あるいは、もっともらしいのは、単一の不正な命令で構成されるmainの定義です。 neverプログラムを実行した場合、まだUBが存在していないと私は主張します。コンパイラ自体は爆発は許されない、IMO。


ブランチ内に可能なまたは証明可能なUBを含む関数

実行の任意のパスに沿ったUBは、以前のすべてのコードを「汚染」するために時間的に後方に到達します。しかし実際には、コンパイラは実際にそのルールを利用することができます証明実行のパスがコンパイル時に表示されるUBにつながるということです。例えば.

_int minefield(int x) {
    if (x == 3) {
        *(char*)nullptr = x/0;
    }

    return x * 5;
}
_

コンパイラーは、_x * 5_がINT_MINおよびINT_MAXで符号付きオーバーフローUBを引き起こす点まで、3以外のすべてのxで機能するasmを作成する必要があります。この関数が_x==3_で呼び出されない場合、プログラムにはもちろんUBが含まれておらず、記述どおりに機能する必要があります。

GNU Cにif(x == 3) __builtin_unreachable();を記述して、xが確実に3ではないことをコンパイラに伝えることもできます。

実際には、通常のプログラムのいたるところに「地雷原」コードがあります。例えば整数による除算は、それがゼロでないことをコンパイラに約束します。ポインターのderefは、それがNULLでないことをコンパイラに約束します。

8
Peter Cordes

ここで「合法」とはどういう意味ですか?これらの標準によれば、C標準またはC++標準に矛盾しないものはすべて合法です。ステートメントを実行するとi = i++;そしてその結果、恐竜は世界を引き継ぐので、それは基準に矛盾しません。しかし、それは物理法則に矛盾するので、それは起こりません:-)

未定義の動作がコンパイラをクラッシュさせる場合、それはCまたはC++標準に違反していません。ただし、コンパイラの品質が向上する可能性がある(そしておそらくそうなるはずです)ことを意味します。

C標準の以前のバージョンでは、エラーまたは未定義の動作に依存しないステートメントがありました。

char* p = 1 / 0;

Char *に定数0を割り当てることができます。ゼロ以外の定数を許可することはできません。 1/0の値は未定義の動作であるため、コンパイラがこのステートメントを受け入れるかどうかは、未定義の動作です。 (最近では、1/0は「整数定数式」の定義に適合しなくなりました)。

3
gnasher729