はじめに
C++の未定義動作(Undefined Behavior)を理解して、安全なコードを書くためのガイド。
「動いてるからOK」じゃないんだよ。UBはコンパイラが「何をしても良い」とされる動作。クラッシュ、データ破壊、セキュリティホールの原因になる。
1. バッファオーバーフロー
// ❌ UB: 配列の範囲外アクセス
int arr[5];
arr[10] = 42; // UB!
// ✅ 安全: 境界チェック
array<int, 5> arr = {1, 2, 3, 4, 5};
try {
cout << arr.at(2) << endl; // OK
arr.at(10); // 例外発生
} catch (const out_of_range& e) {
cout << "Out of range!" << endl;
}
// ✅ span で安全なビュー
span<int> s(arr);
cout << "span size: " << s.size() << endl;
2. 初期化されていない変数
// ❌ UB: 未初期化変数の使用
int x;
cout << x; // UB!
// ✅ 安全: 必ず初期化
int x = 0;
int y{}; // 値初期化(0)
int z = int{}; // 明示的初期化
// ✅ メンバ変数も初期化
struct Safe {
int value = 0; // デフォルトメンバ初期化子
string name{};
};
3. nullポインタのデリファレンス
// ❌ UB: nullポインタのデリファレンス
int* p = nullptr;
*p = 42; // UB!
// ✅ 安全: チェック
int* p = nullptr;
if (p) {
cout << "Value: " << *p << endl;
} else {
cout << "Pointer is null" << endl;
}
// ✅ optional を使う
optional<int> opt;
if (opt.has_value()) {
cout << "Optional: " << *opt << endl;
}
cout << "Optional value: " << opt.value_or(0) << endl;
4. 解放済みメモリ(ダングリングポインタ)
// ❌ UB: 解放済みメモリへのアクセス
int* p = new int(42);
delete p;
*p = 100; // UB!
// ✅ 安全: スマートポインタ
unique_ptr<int> up = make_unique<int>(42);
// 自動解放、ダングリングの心配なし
shared_ptr<int> sp = make_shared<int>(100);
// ✅ weak_ptr で循環参照を避ける
weak_ptr<int> wp = sp;
if (auto locked = wp.lock()) {
cout << *locked << endl;
}
5. 符号付き整数オーバーフロー
// ❌ UB: 符号付き整数オーバーフロー
int max = numeric_limits<int>::max();
int overflow = max + 1; // UB!
// ✅ 安全: オーバーフローチェック
int a = numeric_limits<int>::max();
int b = 1;
if (a > numeric_limits<int>::max() - b) {
cout << "Addition would overflow!" << endl;
} else {
cout << "Safe addition: " << (a + b) << endl;
}
// ✅ 符号なし整数はラップアラウンド(well-defined)
unsigned int ua = numeric_limits<unsigned int>::max();
unsigned int ub = ua + 1; // 0になる(well-defined)
// ✅ より大きな型を使う
int64_t safe_a = a;
int64_t result = safe_a + b;
6. 0除算
// ❌ UB: 整数の0除算
int x = 10 / 0; // UB!
// ✅ 安全: チェック
int a = 10, b = 0;
if (b != 0) {
cout << "Result: " << (a / b) << endl;
} else {
cout << "Division by zero avoided!" << endl;
}
// ✅ optional で結果を返す
auto safe_divide = [](int x, int y) -> optional<int> {
if (y == 0) return nullopt;
return x / y;
};
auto result = safe_divide(10, 2); // 5
result = safe_divide(10, 0); // nullopt
7. 型キャストの問題
// ❌ UB: 不正なreinterpret_cast(strict aliasing violation)
float f = 3.14f;
int* p = reinterpret_cast<int*>(&f);
*p = 42; // UB!
// ✅ 安全: memcpy を使う(型パニング)
float f = 3.14f;
int i;
memcpy(&i, &f, sizeof(i));
// ✅ bit_cast (C++20)
auto bits = bit_cast<int>(f);
// ✅ variant で型安全な値を扱う
variant<int, float, string> v = 3.14f;
if (holds_alternative<float>(v)) {
cout << get<float>(v) << endl;
}
8. 無効なイテレータ
// ❌ UB: 無効化されたイテレータの使用
vector<int> v = {1, 2, 3};
auto it = v.begin();
v.push_back(4); // イテレータ無効化の可能性
*it = 10; // UB!
// ✅ 安全: インデックスを使う
vector<int> v = {1, 2, 3};
size_t idx = 0;
v.push_back(4);
cout << "v[" << idx << "] = " << v[idx] << endl;
// ✅ reserve で再割り当てを防ぐ
vector<int> v2;
v2.reserve(10);
auto it = v2.begin();
v2.push_back(1); // reserve した範囲内なら再割り当てなし
9. シフト演算
// ❌ UB: ビット幅以上のシフト
int x = 1 << 32; // UB! (32ビット環境)
// ❌ UB: 負の値のシフト
int y = -1 << 2; // UB!
// ✅ 安全: 範囲チェック
int shift_amount = 10;
if (shift_amount >= 0 && shift_amount < 32) {
uint32_t result = 1u << shift_amount;
}
// ✅ 符号なし整数を使う
uint32_t x = 1u;
cout << "x << 20 = " << (x << 20) << endl;
10. オブジェクトのライフタイム
// ❌ UB: ローカル変数への参照を返す
int& bad_ref() {
int x = 42;
return x; // UB!
}
// ✅ 安全: 値を返す
int safe_value() {
int x = 42;
return x; // 値コピー
}
// ✅ スマートポインタで管理
unique_ptr<int> safe_ptr() {
return make_unique<int>(100);
}
11. 評価順序
// ❌ UB (C++14以前): 同一式内で同じ変数を複数回変更
int i = 0;
int x = i++ + ++i; // UB!
// ✅ 安全: 明示的に分離
int i = 0;
int a = i++;
int b = ++i;
int x = a + b;
安全なコードのガイドライン
| ガイドライン |
|---|
| 1. 変数は必ず初期化する |
| 2. 生ポインタの代わりにスマートポインタを使う |
3. 配列の代わりにstd::array/std::vectorを使う |
4. .at()で境界チェックする |
| 5. nullチェックをする、またはoptionalを使う |
| 6. 整数演算の前にオーバーフローをチェック |
| 7. 除算の前に0チェック |
| 8. C++コアガイドラインに従う |
| 9. 静的解析ツールを使う |
10. -fsanitize=undefinedでUBを検出 |
ツールによる検出
# UBサニタイザー
g++ -fsanitize=undefined program.cpp -o program
# アドレスサニタイザー
g++ -fsanitize=address program.cpp -o program
# 静的解析
clang-tidy program.cpp
まとめ
未定義動作を避けることで、安全で堅牢なC++プログラムを作成できます。モダンC++の機能(スマートポインタ、optional、span等)を活用し、サニタイザーで検証しましょう!