テストも通った。本番でも動いた。でも「正しい」とは限らない
こんなコードがあります。
int arr[3] = {10, 20, 30};
std::cout << arr[5] << std::endl; // 配列の範囲外アクセス
手元の環境で実行すると、なぜか 0 が出力されて、クラッシュもしません。
「あれ、範囲外アクセスしてるのにエラーにならないぞ。じゃあ大丈夫か」
大丈夫ではありません。
C++の世界には 「未定義動作(Undefined Behavior = UB)」 という概念があります。
これは「何が起きても文句は言えない。クラッシュするかもしれないし、正常に動くかもしれないし、ハードディスクをフォーマットするかもしれない」という、コンパイラからの最後通告です。
(実際にハードディスクをフォーマットすることはまずありませんが、C++の規格上は「何でも起こりうる」と定義されています)
なぜ「動いている」のに危険なのか
UBの最も恐ろしい点は、「今の環境ではたまたま動いている」だけで、以下のどれかで突然壊れることです。
- コンパイラのバージョンを上げた
- 最適化オプションを
-O2に変えた - 別のOS/アーキテクチャでビルドした
- 無関係な場所のコードを変更した
コンパイラの最適化がUBを「消す」例
bool isNegative(int x) {
return x + 100 < x; // 符号付き整数のオーバーフロー = UB
}
数学的には、x が非常に大きい場合 x + 100 がオーバーフローして負の数になり、true を返すことがあります。
しかしコンパイラは「符号付き整数のオーバーフローはUBだから、起きないと仮定してよい」と判断し、この関数を最適化で return false; に書き換えます。
コンパイラの思考:
「C++の規格上、符号付きオーバーフローは起きない(UBだから)。
ということは x + 100 は常に x より大きい。
つまりこの条件式は常に false。最適化で消そう。」
あなたが書いたロジックが、コンパイラによって予告なく削除される。これがUBの真の恐怖です。
代表的な未定義動作チートシート
❌ 配列の範囲外アクセス
❌ 符号付き整数のオーバーフロー
❌ ヌルポインタのデリファレンス
❌ 初期化されていない変数の使用
❌ delete 済みメモリへのアクセス(use-after-free)
❌ データ競合(マルチスレッドでの同時読み書き)
❌ 厳密なエイリアシング規則の違反
対策:UBを検出する武器
1. コンパイラの警告を最大化する
g++ -Wall -Wextra -Werror -Wpedantic main.cpp
-Werror は警告をエラーとして扱い、無視を許しません。
2. サニタイザーを使う(最強の武器)
g++ -fsanitize=undefined -fsanitize=address main.cpp
./a.out
# 実行時にUBを検知して即座にエラーメッセージを出力してくれる
-
UBSan(
-fsanitize=undefined):未定義動作を実行時に検出 -
ASan(
-fsanitize=address):メモリ関連のバグを検出
3. 静的解析ツールの導入
clang-tidy、cppcheckなどの静的解析ツールでコンパイル前にUBの種を刈り取りましょう。
C++は「信頼するが検証しない」言語です。コンパイラはあなたのコードにUBがないと善意で仮定し、その仮定に基づいて攻撃的な最適化を行います。「動いてるから正しい」と思った瞬間、あなたはUBの深淵に片足を踏み入れています。