未定義な振る舞い
From cppreference.com
言語の特定のルールが破られた場合、プログラム全体を無意味にします。
目次 |
[edit] 説明
C++ 標準は、以下のいずれかのクラスに該当しないすべての C++ プログラムの観測可能な動作を正確に定義します
- 形式不適格 (ill-formed) - プログラムに構文エラーまたは診断可能な意味エラーがある場合。
- 適合する C++ コンパイラは、そのようなコードに意味を割り当てる言語拡張を定義している場合でも (可変長配列の場合など)、診断を発行する必要があります。
- 標準のテキストでは、これらの要件を示すために shall、shall not、および ill-formed を使用します。
- 形式不適格、診断不要 (ill-formed, no diagnostic required) - プログラムに一般的に診断できない意味エラーがある場合 (例: ODR の違反や、リンク時のみ検出可能なその他のエラー)。
- そのようなプログラムが実行された場合、動作は未定義になります。
- 実装定義の動作 (implementation-defined behavior) - プログラムの動作は実装によって異なり、適合する実装は各動作の結果を文書化する必要があります。
- 例としては、std::size_t の型やバイト内のビット数、std::bad_alloc::what のテキストなどがあります。
- 実装定義の動作のサブセットは、実装によって提供されるロケールに依存するロケール固有の動作 (locale-specific behavior) です。
- 不特定な動作 (unspecified behavior) - プログラムの動作は実装によって異なり、適合する実装は各動作の結果を文書化する必要はありません。
|
(C++26以降) |
- 未定義の動作 (undefined behavior) - プログラムの動作に制限はありません。
|
(C++11以降) |
[edit] UB と最適化
正しい C++ プログラムは未定義の動作を含まないため、UB を持つプログラムを最適化を有効にしてコンパイルすると、コンパイラは予期しない結果を生成する可能性があります
例えば、
[edit] 符号付きオーバーフロー
int foo(int x) { return x + 1 > x; // either true or UB due to signed overflow }
(デモ) としてコンパイルされる可能性があります。
foo(int): mov eax, 1 ret
[edit] 範囲外アクセス
int table[4] = {}; bool exists_in_table(int v) { // return true in one of the first 4 iterations or UB due to out-of-bounds access for (int i = 0; i <= 4; i++) if (table[i] == v) return true; return false; }
(デモ) としてコンパイルされる可能性があります。
exists_in_table(int): mov eax, 1 ret
[edit] 未初期化のスカラー
std::size_t f(int x) { std::size_t a; if (x) // either x nonzero or UB a = 42; return a; }
(デモ) としてコンパイルされる可能性があります。
f(int): mov eax, 42 ret
表示された出力は、以前のバージョンの gcc で観測されました。
このコードを実行
実行結果の例
p is true p is false
[edit] 無効なスカラー
int f() { bool b = true; unsigned char* p = reinterpret_cast<unsigned char*>(&b); *p = 10; // reading from b is now UB return b == 0; }
(デモ) としてコンパイルされる可能性があります。
f(): mov eax, 11 ret
[edit] ヌルポインタの逆参照
この例は、ヌルポインタの逆参照の結果からの読み取りを示しています。
int foo(int* p) { int x = *p; if (!p) return x; // Either UB above or this branch is never taken else return 0; } int bar() { int* p = nullptr; return *p; // Unconditional UB }
(デモ) としてコンパイルされる可能性があります。
foo(int*): xor eax, eax ret bar(): ret
[edit] 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
[edit] 副作用のない無限ループ
表示された出力を確認するには、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!
[edit] 診断メッセージ付きの形式不適格
コンパイラは、形式不適格なプログラムに意味を与える方法で言語を拡張することが許可されていることに注意してください。そのような場合、C++ 標準が要求するのは診断メッセージ (コンパイラの警告) のみです。ただし、プログラムが「診断不要の形式不適格」である場合を除きます。
たとえば、--pedantic-errors を介して言語拡張が無効にされていない限り、GCC は次の例を警告のみでコンパイルします。これは、それが「エラー」の例としてC++ 標準に記載されているにもかかわらずです (「GCC Bugzilla #55783」も参照)。
このコードを実行
#include <iostream> // Example tweak, do not use constant double a{1.0}; // C++23 standard, §9.4.5 List-initialization [dcl.init.list], Example #6: struct S { // no initializer-list constructors S(int, double, double); // #1 S(); // #2 // ... }; S s1 = {1, 2, 3.0}; // OK, invoke #1 S s2{a, 2, 3}; // error: narrowing S s3{}; // OK, invoke #2 // — end example] 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.[edit] 参考文献
| 拡張コンテンツ |
|---|
|
[edit] 関連項目
[[assume(expression)]](C++23) |
特定の時点でexpressionが常にtrueに評価されることを指定します (属性指定子) |
[[indeterminate]](C++26) |
オブジェクトが初期化されていない場合、不定値を持つことを指定します。 (属性指定子) |
| (C++23) |
実行が到達不能な地点をマークする (関数) |
| C ドキュメント 未定義の動作
| |
[edit] 外部リンク
| 1. | The LLVM Project Blog: C プログラマーが未定義の動作について知っておくべきこと #1/3 |
| 2. | The LLVM Project Blog: C プログラマーが未定義の動作について知っておくべきこと #2/3 |
| 3. | The LLVM Project Blog: C プログラマーが未定義の動作について知っておくべきこと #3/3 |
| 4. | 未定義の動作はタイムトラベルを引き起こす可能性がある (他のこともそうだが、タイムトラベルが最も奇妙だ) |
| 5. | C/C++ における整数オーバーフローの理解 |
| 6. | ヌルポインタの楽しみ、パート 1 (ヌルポインタの逆参照による UB が原因で発生した Linux 2.6.30 のローカルエクスプロイト) |
| 7. | 未定義の動作とフェルマーの最終定理 |
| 8. | C++ プログラマーのための未定義の動作ガイド |