はじめに
C++の例外安全性には3つのレベルがある。
「例外が飛んでもリソースリークしない」とか「完全にロールバックできる」とか、ちゃんと意識して書いてる?
例外安全性の3つのレベル
| レベル | 説明 |
|---|---|
| Basic(基本保証) | 例外発生時もリソースリークなし、オブジェクトは有効状態 |
| Strong(強い保証) | 例外発生時、操作前の状態に完全ロールバック |
| Nothrow(無例外保証) | 例外を投げない |
1. 基本保証の例
class BasicSafe {
int* data;
size_t size;
public:
BasicSafe(size_t n) : data(new int[n]), size(n) {
for (size_t i = 0; i < n; ++i) data[i] = 0;
}
~BasicSafe() { delete[] data; }
// コピー代入(基本保証)
BasicSafe& operator=(const BasicSafe& other) {
if (this != &other) {
int* new_data = new int[other.size]; // 先に確保
memcpy(new_data, other.data, other.size * sizeof(int));
delete[] data; // 古いデータを解放
data = new_data;
size = other.size;
}
return *this;
}
};
ポイント: 新しいリソースを確保してから古いリソースを解放
2. 強い保証(copy-and-swap イディオム)
class StrongSafe {
int* data;
size_t size;
public:
StrongSafe(size_t n) : data(new int[n]), size(n) {}
~StrongSafe() { delete[] data; }
StrongSafe(const StrongSafe& other)
: data(new int[other.size]), size(other.size) {
memcpy(data, other.data, size * sizeof(int));
}
// swap は nothrow
friend void swap(StrongSafe& a, StrongSafe& b) noexcept {
using std::swap;
swap(a.data, b.data);
swap(a.size, b.size);
}
// 強い保証: copy-and-swap
StrongSafe& operator=(StrongSafe other) { // 値渡しでコピー
swap(*this, other); // nothrow
return *this;
} // other のデストラクタが古いデータを解放
// 強い保証のpush_back
void push_back(int value) {
int* new_data = new int[size + 1]; // 失敗したら例外
memcpy(new_data, data, size * sizeof(int));
new_data[size] = value;
// ここまで成功したら入れ替え
delete[] data;
data = new_data;
++size;
}
};
ポイント: 全ての変更を準備してから、nothrowな操作で適用
3. nothrow保証
class NothrowSafe {
int value;
public:
NothrowSafe(int v = 0) noexcept : value(v) {}
~NothrowSafe() noexcept = default;
NothrowSafe(const NothrowSafe&) noexcept = default;
NothrowSafe& operator=(const NothrowSafe&) noexcept = default;
NothrowSafe(NothrowSafe&&) noexcept = default;
NothrowSafe& operator=(NothrowSafe&&) noexcept = default;
int get() const noexcept { return value; }
void set(int v) noexcept { value = v; }
friend void swap(NothrowSafe& a, NothrowSafe& b) noexcept {
int tmp = a.value;
a.value = b.value;
b.value = tmp;
}
};
// noexcept チェック
static_assert(is_nothrow_copy_constructible_v<NothrowSafe>);
static_assert(is_nothrow_move_constructible_v<NothrowSafe>);
4. RAII によるリソース管理
class FileHandle {
FILE* fp;
public:
explicit FileHandle(const char* filename, const char* mode)
: fp(fopen(filename, mode)) {
if (!fp) throw runtime_error("Failed to open file");
}
~FileHandle() noexcept {
if (fp) fclose(fp);
}
// コピー禁止
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// ムーブ可能
FileHandle(FileHandle&& other) noexcept : fp(other.fp) {
other.fp = nullptr;
}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (fp) fclose(fp);
fp = other.fp;
other.fp = nullptr;
}
return *this;
}
FILE* get() const noexcept { return fp; }
};
// 使用例 - 例外が発生してもファイルは必ず閉じられる
void safe_file_operation() {
FileHandle fh("data.txt", "w");
// 処理...
} // 自動的にクローズ
5. ScopeGuard パターン
template<typename F>
class ScopeGuard {
F func;
bool active;
public:
explicit ScopeGuard(F f) : func(std::move(f)), active(true) {}
~ScopeGuard() {
if (active) func();
}
void dismiss() noexcept { active = false; }
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
};
template<typename F>
ScopeGuard<F> make_scope_guard(F f) {
return ScopeGuard<F>(std::move(f));
}
// 使用例
void example() {
int* ptr = new int(42);
auto guard = make_scope_guard([&] { delete ptr; });
// 処理...
// 例外が発生してもptrは必ず解放される
guard.dismiss(); // 正常終了時はクリーンアップ不要
}
6. トランザクション的な操作
class Account {
int balance;
string name;
public:
Account(const string& n, int b) : balance(b), name(n) {}
// 強い保証: 成功か完全なロールバック
static void transfer(Account& from, Account& to, int amount) {
if (from.balance < amount) {
throw runtime_error("Insufficient balance");
}
// 両方の変更を一時変数で計算
int new_from = from.balance - amount;
int new_to = to.balance + amount;
// 計算成功後に実際に適用(nothrow)
from.balance = new_from;
to.balance = new_to;
}
};
7. std::vectorの例外安全性
// push_back は強い保証
// reserve はメモリ確保に失敗したら例外
// swap は nothrow
vector<int> v = {1, 2, 3};
v.push_back(4); // 強い保証
v.reserve(100); // 失敗時は例外
vector<int> v2 = {10, 20};
swap(v, v2); // nothrow
実行結果
=== 基本保証 ===
Copied: 10, 20
Assigned: 10, 20
=== 強い保証 ===
After push_back: size = 4
Last element: 100
=== nothrow保証 ===
Before swap: a=10, b=20
After swap: a=20, b=10
is_nothrow_copy_constructible: 1
is_nothrow_move_constructible: 1
=== RAII ===
File written successfully
File handle automatically closed
=== ScopeGuard ===
Resource acquired: 1
Resource cleaned up by scope guard
After scope: resource = 0
=== トランザクション ===
Before: Alice=1000, Bob=500
After transfer(300): Alice=700, Bob=800
Transfer failed: Insufficient balance
State preserved: Alice=700, Bob=800
ベストプラクティス
- デストラクタはnoexcept: 例外を投げない
- swapはnoexcept: 強い保証の基礎
- ムーブ操作はnoexcept: パフォーマンスとSTL互換性
- RAII: リソース管理の基本
- copy-and-swap: 強い保証の実装パターン
まとめ
| 操作 | 推奨される保証 |
|---|---|
| デストラクタ | nothrow |
| swap | nothrow |
| ムーブ | nothrow |
| push_back | strong |
| operator= | strong(copy-and-swap) |