Undefined behavior
言語の特定の規則に違反した場合、プログラム全体を無意味なものとしてレンダリングします。
目次 |
説明
C++標準は、以下の分類に該当しないすべてのC++プログラムの 観測可能な動作 を厳密に定義しています:
- ill-formed - プログラムに構文エラーまたは診断可能な意味論的エラーがあります。
-
- 準拠したC++コンパイラは、そのようなコードに意味を割り当てる言語拡張(可変長配列など)を定義している場合でも、診断メッセージを出力することが要求されます。
- 標準規格の本文では、 shall 、 shall not 、および ill-formed という用語を使用してこれらの要件を示しています。
- ill-formed、 no diagnostic required - プログラムは意味論的な誤りを含み、一般的なケースでは診断不可能な場合がある(例: ODR 違反やリンク時にのみ検出可能なその他のエラー)。
-
- そのようなプログラムが実行された場合、動作は未定義です。
- implementation-defined behavior - プログラムの動作は実装によって異なり、適合実装は各動作の影響を文書化しなければならない。
-
- 例えば、 std::size_t の型や、1バイトのビット数、 std::bad_alloc::what のテキストなど。
- 実装定義動作の一部は ロケール固有動作 であり、実装が提供する locale に依存します。
- 未規定動作 (unspecified behavior) - プログラムの動作は実装によって異なり、適合する実装は各動作の影響を文書化する必要はありません。
-
- 例えば、 評価順序 、同一の string literals が区別されるかどうか、配列割り当てのオーバーヘッド量など。
- 各々の未規定動作は、有効な結果の集合のうちの1つをもたらす。
|
(C++26以降) |
- undefined behavior - プログラムの動作に制限はありません。
-
- 未定義動作の例としては、データ競合、配列境界外へのメモリアクセス、符号付き整数オーバーフロー、nullポインタのデリファレンス、 同一スカラーに対する複数回の 変更 (中間シーケンスポイントなし) (C++11まで) (順序付けされていない) (C++11以降) 、 異なる型のポインタを通じたオブジェクトへのアクセス などが挙げられる。
- 実装は未定義動作の診断を要求されず(ただし多くの単純な状況は診断される)、コンパイルされたプログラムは有意義な動作をすることが要求されない。
|
(C++11以降) |
UBと最適化
正しいC++プログラムは未定義動作を含まないため、実際にUBを持つプログラムが最適化を有効にしてコンパイルされると、コンパイラは予期しない結果を生成する可能性があります:
例えば、
符号付きオーバーフロー
int foo(int x) { return x + 1 > x; // true または符号付きオーバーフローによる未定義動作 }
以下のようにコンパイルされる可能性があります ( demo )
foo(int): mov eax, 1 ret
範囲外アクセス
int table[4] = {}; bool exists_in_table(int v) { // 最初の4回の反復のいずれかでtrueを返す、さもなければ範囲外アクセスによる未定義動作が発生 for (int i = 0; i <= 4; i++) if (table[i] == v) return true; return false; }
以下のようにコンパイルされる可能性があります ( demo )
exists_in_table(int): mov eax, 1 ret
未初期化スカラー
std::size_t f(int x) { std::size_t a; if (x) // xが非ゼロ、または未定義動作 a = 42; return a; }
以下のようにコンパイルされる可能性があります ( demo )
f(int): mov eax, 42 ret
表示されている出力は、古いバージョンのgccで観察されたものです
出力例:
p is true p is false
無効なスカラー
int f() { bool b = true; unsigned char* p = reinterpret_cast<unsigned char*>(&b); *p = 10; // bからの読み取りは未定義動作となる return b == 0; }
以下のようにコンパイルされる可能性があります ( demo )
f(): mov eax, 11 ret
ヌルポインタデリファレンス
この例は、nullポインタのデリファレンス結果からの読み取りを示しています。
int foo(int* p) { int x = *p; if (!p) return x; // 上記のUB、またはこの分岐は決して実行されない else return 0; } int bar() { int* p = nullptr; return *p; // 無条件のUB }
以下のようにコンパイルされる可能性があります ( demo )
foo(int*): xor eax, eax ret bar(): ret
std::realloc に渡されたポインタへのアクセス std::realloc
clangを選択して表示される出力を確認してください
#include <cstdlib> #include <iostream> int main() { int* p = (int*)std::malloc(sizeof(int)); int* q = (int*)std::realloc(p, sizeof(int)); *p = 1; // UB access to a pointer that was passed to realloc *q = 2; if (p == q) // UB access to a pointer that was passed to realloc std::cout << *p << *q << '\n'; }
出力例:
12
副作用のない無限ループ
clangまたは最新のgccを選択して、表示される出力を確認してください。
#include <iostream> bool fermat() { const int max_value = 1000; // Non-trivial infinite loop with no side effects is UB for (int a = 1, b = 1, c = 1; true; ) { if (((a * a * a) == ((b * b * b) + (c * c * c)))) return true; // disproved :() a++; if (a > max_value) { a = 1; b++; } if (b > max_value) { b = 1; c++; } if (c > max_value) c = 1; } return false; // not disproved } int main() { std::cout << "Fermat's Last Theorem "; fermat() ? std::cout << "has been disproved!\n" : std::cout << "has not been disproved.\n"; }
出力例:
Fermat's Last Theorem has been disproved!
診断メッセージ付きの不適格形式
コンパイラは、不適格なプログラムに意味を与える方法で言語を拡張することが許可されていることに注意してください。C++標準がこのような場合に要求する唯一のことは診断メッセージ(コンパイラ警告)であり、プログラムが「診断が要求されない不適格」でない限りです。
例えば、言語拡張が
--pedantic-errors
によって無効にされていない限り、GCCは以下の例を
警告のみでコンパイルします
。これはC++標準で「エラー」の例として
記載されている
にもかかわらずです(
GCC Bugzilla #55783
も参照)。
#include <iostream> // 例の調整、定数を使用しないこと double a{1.0}; // C++23標準、§9.4.5 リスト初期化 [dcl.init.list]、例 #6: struct S { // 初期化子リストコンストラクタなし S(int, double, double); // #1 S(); // #2 // ... }; S s1 = {1, 2, 3.0}; // OK、#1を呼び出し S s2{a, 2, 3}; // エラー: 縮小変換 S s3{}; // OK、#2を呼び出し // — 例終わり] S::S(int, double, double) {} S::S() {} int main() { std::cout << "All checks have passed.\n"; }
出力例:
main.cpp:17:6: error: type 'double' cannot be narrowed to 'int' in initializer ⮠
list [-Wc++11-narrowing]
S s2{a, 2, 3}; // error: narrowing
^
main.cpp:17:6: note: insert an explicit cast to silence this issue
S s2{a, 2, 3}; // error: narrowing
^
static_cast<int>( )
1 error generated.
参考文献
| 拡張コンテンツ |
|---|
|
関連項目
[[
assume
(
expression
)]]
(C++23)
|
指定された時点で
式
が常に
true
と評価されることを指定する
(属性指定子) |
[[
indeterminate
]]
(C++26)
|
初期化されていない場合、オブジェクトが不定値を持つことを指定する
(属性指定子) |
|
(C++23)
|
到達不能な実行ポイントをマークする
(関数) |
|
Cドキュメント
を参照
未定義動作
|
|
外部リンク
| 1. | LLVMプロジェクトブログ: Cプログラマーが未定義動作について知っておくべきこと #1/3 |
| 2. | LLVMプロジェクトブログ: Cプログラマーが未定義動作について知っておくべきこと #2/3 |
| 3. | LLVMプロジェクトブログ: Cプログラマーが未定義動作について知っておくべきこと #3/3 |
| 4. | 未定義動作はタイムトラベルを引き起こす可能性がある(他にも様々な影響があるが、タイムトラベルが最も奇妙) |
| 5. | C/C++における整数オーバーフローの理解 |
| 6. | NULLポインタで遊ぶ、パート1 (nullポインタ参照による未定義動作が原因のLinux 2.6.30でのローカルエクスプロイト) |
| 7. | 未定義動作とフェルマーの最終定理 |
| 8. | C++プログラマーのための未定義動作ガイド |