未定義動作
提供: cppreference.com
言語の一定のルールに違反すると、プログラム全体が無意味になります。
説明
C++ 標準は、以下の分類のいずれにも当てはならないすべての C++ プログラムの観察可能な動作を、正確に定義します。
- ill-formed ー プログラムに構文の誤りまたは診断可能な意味論上の誤りがあります。 準拠した C++ コンパイラは、たとえそのようなコードに意味を与える言語拡張 (可変長配列など) を定義していたとしても、診断を発行することが要求されます。 標準の文章はこれらの要件を示すために、すべきです、すべきではありません、および ill-formed という言葉を用います。
- ill-formed (診断は要求されない) ー プログラムに一般的なケースでは診断可能でないかもしれない意味論上の誤り (例えば ODR の違反やリンク時にしか検出できないその他の誤り) があります。 そのようなプログラムが実行された場合、動作は未定義です。
- 処理系定義の動作 ー プログラムの動作は処理系によって様々であり、準拠した処理系は各動作の効果を文章化しなければなりません。 例えば、 std::size_t の型、1バイトのビット数、 std::bad_alloc::what のテキストなどです。 処理系定義の動作の中にはロケール固有の動作があり、処理系が供給しているロケールに依存します。
- 未規定の動作 ー プログラムの動作は処理系によって様々であり、準拠した処理系は各動作の効果を文章化することは要求されません。 例えば、評価順序、同一の文字列リテラルが区別可能かどうか、配列確保のオーバーヘッドの量などです。 各未規定の動作の結果は有効な結果の集合のいずれかになります。
- 未定義の動作 ー プログラムの動作について何の制約もありません。 未定義動作の例は、配列の境界の外側のメモリアクセス、符号付き整数のオーバーフロー、ヌルポインタの逆参照、副作用完了点のないひとつの式内で同じスカラーを2回以上変更する、などです。 コンパイラは未定義動作を診断することは要求されず (単純な状況は診断されることも多いですが)、コンパイルされたプログラムはいかなる意味のある動作を行うことも要求されません。
未定義動作と最適化
正しい C++ プログラムは未定義動作を含まないため、実際には未定義動作を持つプログラムを最適化を有効にしてコンパイルするとき、コンパイラは予測できない結果を生むことがあります。
例えば、
符号付きのオーバーフロー
int foo(int x) {
return x+1 > x; // true または符号付きオーバーフローによる未定義動作のいずれか
}
これは以下のようにコンパイルされることがあります (デモ)。
foo(int):
movl $1, %eax
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;
}
これは以下のようにコンパイルされることがあります (デモ)。
exists_in_table(int):
movl $1, %eax
ret
未初期化のスカラー
std::size_t f(int x)
{
std::size_t a;
if(x) // x が非ゼロであるか、そうでなければ未定義動作
a = 42;
return a;
}
これは以下のようにコンパイルされることがあります (デモ)。
f(int):
mov eax, 42
ret
以下で示されている出力例は古いバージョンの gcc で観察されたものです。
Run this code
bool p; // 未初期化のローカル変数
if(p) // 未初期化スカラーへの未定義動作なアクセス
std::puts("p is true");
if(!p) // 未初期化スカラーへの未定義動作なアクセス
std::puts("p is false");
出力例:
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;
}
これは以下のようにコンパイルされることがあります (デモ)。
f():
movl $11, %eax
ret
ヌルポインタの逆参照
int foo(int* p) {
int x = *p;
if(!p) return x; // 上の *p が未定義動作であるか、そうでなければこの分岐に入ることはありません
else return 0;
}
int bar() {
int* p = nullptr;
return *p; // 無条件の未定義動作
}
これは以下のようにコンパイルされることがあります (gcc を用いた foo と clang を用いた bar)
foo(int*):
xorl %eax, %eax
ret
bar():
retq
realloc に渡したポインタへのアクセス
以下に示されている出力例を観察するには clang を選択してください。
Run this code
#include <iostream>
#include <cstdlib>
int main() {
int *p = (int*)std::malloc(sizeof(int));
int *q = (int*)std::realloc(p, sizeof(int));
*p = 1; // realloc に渡したポインタへの未定義動作なアクセス
*q = 2;
if (p == q) // realloc に渡したポインタへの未定義動作なアクセス
std::cout << *p << *q << '\n';
}
出力例:
12
副作用なしの無限ループ
以下に示されている出力例を観察するためには clang を選択してください。
Run this code
#include <iostream>
int fermat() {
const int MAX = 1000;
int a=1,b=1,c=1;
// 副作用なしの無限ループは未定義動作
while (1) {
if (((a*a*a) == ((b*b*b)+(c*c*c)))) return 1;
a++;
if (a>MAX) { a=1; b++; }
if (b>MAX) { b=1; c++; }
if (c>MAX) { c=1;}
}
return 0;
}
int main() {
if (fermat())
std::cout << "Fermat's Last Theorem has been disproved.\n";
else
std::cout << "Fermat's Last Theorem has not been disproved.\n";
}
出力例:
Fermat's Last Theorem has been disproved.