第37回 RAII の哲学
STEP 5 で学んだ RAII を「哲学」として捉え直します。スマートポインタに入る前の導入章。核心は 3 つ:「所有権は 1 人だけ」「リソース = オブジェクト」「関数の出口は 1 つで済む」。この考え方が、C++ 標準ライブラリ全体と、あなたが書く良いコードを貫く筋になります。
このページで押さえること
✅ 最低限ここだけ覚える
- リソースには必ず所有者(= オブジェクト)が 1 人いる
- オブジェクトが死ぬと資源が解放される
- コピー・ムーブは「所有権の複製・移動」と読み替える
- 次章からのスマートポインタはこの哲学の具現化
⭐ 余裕があれば読む
- 所有 vs 借用(ownership vs borrowing)
- Rust との比較
- 例外安全性の 3 段階
- 共有所有 vs 単独所有
1. リソースとは何か
C++ で言うリソースとは、「確保したら必ず解放が必要なもの」全般。メモリだけではありません。
| リソース | 取得 | 解放 | C++ での RAII ラッパ |
| ヒープメモリ | new / malloc | delete / free | std::unique_ptr, std::shared_ptr, std::vector, std::string |
| ファイル | fopen | fclose | std::fstream |
| mutex ロック | mtx.lock() | mtx.unlock() | std::lock_guard, std::unique_lock |
| スレッド | thread 起動 | join() / detach() | std::jthread (C++20), RAII ラッパ |
| ソケット | socket() | close() | 自作 RAII か Boost.Asio 等 |
| DB 接続 | 開く | 閉じる | 各DBライブラリのコネクション型 |
リソース = 「取得したら必ず解放が必要なもの」すべて
C 時代の「取得」と「解放」はペアの関数呼び出しでした。両方を正しい順序で呼ぶ責任がプログラマにありました。C++ の RAII は、この責任をオブジェクトに移譲します。
2. 所有権の考え方
RAII の心臓部は所有権 (ownership) の概念です。すべてのリソースには1 人の所有者(= オブジェクト)がいて、その所有者が責任を持って解放する、と設計します。
1 つのリソースには 1 人の明確な所有者。
所有者が死んだら資源も解放される。
所有権の生涯
所有権の流れRAII
// ① 所有権の誕生(取得)
FileHandle f{"data.txt"}; // ctor でファイルを開く
// ② 所有権の使用
f.read(); // メンバ関数で操作
// ③ 所有権の終焉(解放)
// スコープ終了時、dtor で自動的にファイルを閉じる
所有権をどう扱うか
- コピー:所有権を複製する(2 人が同じ資源を管理) → shared_ptr 的な概念
- ムーブ:所有権を移動する(元の所有者は手放す) → unique_ptr 的な概念
- 参照・ポインタ:所有権なしで見るだけ(借用)
コピーとムーブの読み替え:
- コピーコンストラクタ = 「リソースを 2 つ作って、両方を管理」
- ムーブコンストラクタ = 「リソースは 1 つのまま、所有者を移動」
リソースの性質によって、どちらを許可するかが決まります。たとえば「ファイル」はどちらかと言えばムーブ、「数値リスト」はコピーが自然、「システムの一意な ID」はコピーもムーブも禁止(特別な設計が必要)。
3. 所有 vs 借用
ある関数や構造体がリソースを所有しているか借用しているだけかを区別するのが設計の出発点。C++ 標準ライブラリの型を眺めると、このパターンが貫かれています。
| 種類 | 意味 | 代表例 |
| 所有(値型 / 所有ポインタ) | 自分が責任を持って解放 | std::string, std::vector, std::unique_ptr, std::shared_ptr |
| 借用(参照 / 非所有ポインタ) | 他人が管理、自分は見るだけ | std::string_view, std::span, 生ポインタ T*, 参照 T&, std::weak_ptr |
関数引数での原則
シグネチャで意図を示す設計
// 借用:関数内で使うだけ、所有権は渡さない
void print(const std::string& s); // 読むだけ
void trim(std::string& s); // 書き換え
// 所有:関数に渡したら呼び出し側は手放す
void store(std::string s); // 値渡し: 内部でコピー or ムーブ
void store(std::unique_ptr<T> p); // unique_ptr 渡し: 明示的な所有権移転
関数のシグネチャを見れば「この関数は所有権を取るのか、見るだけなのか」が利用者に伝わるように書くのが良い C++ コードの指標。
4. 例外安全性の 3 段階
RAII の最大の価値は例外安全性。例外が飛んでもリソースリークが起きません。例外安全性には 3 つのレベルがあります。
基本保証Basic
// 例外が飛んでも
// ・リソースリークしない
// ・プログラムは有効な状態
// (ただし中身は変わっているかも)
強い保証Strong
// 例外が飛んだら
// ・呼び出し前の状態に戻る
// (トランザクション的)
no-throw 保証noexcept
// そもそも例外を投げない
// (ムーブ・swap・デストラクタに要求される)
C++ 標準の原則:
- デストラクタは絶対に例外を投げない(暗黙
noexcept)
- ムーブコンストラクタ/ムーブ代入は可能な限り noexcept に
- swap はnoexcept が理想
これらが守られると、例外安全な標準ライブラリが全体として機能します。
⭐
ここから次のスマートポインタへ
§5 は次章の導入。RAII の哲学が具体的な型に結実します。
5. スマートポインタへの導入
STEP 5 までで学んだ RAII 思想を、「ポインタの所有権管理」に特化した型として提供するのがスマートポインタです。
| 型 | 所有権モデル | 次回扱う章 |
std::unique_ptr<T> | 単独所有(コピー不可、ムーブのみ) | 第33回 |
std::shared_ptr<T> | 共有所有(参照カウント) | 第34回 ★ |
std::weak_ptr<T> | 借用(shared_ptr への非所有参照) | 第35回 |
スマートポインタの核心思想
// 昔: new と delete を自分で管理
T* p = new T();
// ... (例外が飛ぶとリーク)
delete p;
// モダン: 所有権を型が管理
auto p = std::make_unique<T>();
// ... (例外が飛んでも dtor で delete される)
// スコープ終了で自動 delete
new / delete を
アプリコードに書かないのがモダン C++
生の new / delete は、スマートポインタの実装内部やパフォーマンスクリティカルな低レベルでのみ登場すべきもの。普段のアプリケーションコードにはほぼ必要ありません。
確認クイズ
RAII の哲学を 4 問で確認。
Q1. 次のうち「リソース」として扱うべきでないのは?
ヒープメモリ
ファイルハンドル
ローカル変数の int
mutex ロック
リソース =「取得したら解放が必要なもの」。ローカル変数の int は特別な解放手続きが不要(スタックから消えるだけ)。
Q2. RAII の「所有権」モデルで、ムーブは何に対応する?
所有権の複製
所有権の破棄
所有権の移動
借用
ムーブは「所有権の移動」。元の所有者は所有権を失い、新しい所有者が責任を引き継ぎます。コピーは「複製」で 2 人の所有者が発生する。
Q3. 関数引数で「所有権を渡す」意図を示す型は?
const T&
T&
std::string_view
std::unique_ptr<T> (値渡し)
unique_ptr の値渡しは「呼び出し側が所有権を手放し、関数側に移動させる」明確な意図のシグネチャ。参照は借用、string_view も借用。
Q4. デストラクタに求められる例外安全性レベルは?
基本保証(Basic)
強い保証(Strong)
no-throw 保証(noexcept)
特にない
デストラクタは例外を投げてはいけません。投げるとスタック巻き戻し中に新たな例外が発生して std::terminate が呼ばれます。C++11 以降、デストラクタはデフォルトで暗黙の noexcept。