C++ Learning

第31回 デストラクタと RAII ★目玉

オブジェクトがスコープを抜けるときに自動で呼ばれる終了処理がデストラクタ。これをリソース管理に応用したのが C++ 最大の発明 RAII(Resource Acquisition Is Initialization)。C の malloc/freefopen/fclosepthread_mutex_lock/unlock といった「対になる処理の後半を忘れる問題」を、コンパイラに任せて根絶する仕組みです。

このページで押さえること
✅ 最低限ここだけ覚える
  • デストラクタ = ~ClassName() で書く
  • オブジェクトがスコープを抜ける瞬間に自動で呼ばれる
  • RAII = 「リソース取得をコンストラクタ、解放をデストラクタ」で管理
  • vector / string / unique_ptr などは全部 RAII 実装
⭐ 余裕があれば読む
  • スタック上とヒープ上でタイミングの違い
  • スコープガード(std::lock_guard 的な型)
  • 例外安全性と RAII の関係
  • 仮想デストラクタ(継承時、第 36 回)

1. まず触ってみる ― スコープアニメ

下のボタンで、コードを 1 行ずつ進めてみてください。「{」でオブジェクトが作られ、「}」で自動的に破棄される様子が見えます。

▶ オブジェクトのライフサイクル
step 0 / 10
class Greeter {
std::string name_;
public:
Greeter(std::string n) : name_(std::move(n)) { log("ctor " + name_); }
~Greeter() { log("dtor " + name_); }
};
int main() {
Greeter alice("Alice"); // ← ctor
{
Greeter bob("Bob"); // ← ctor
} // ← bob の dtor
Greeter carol("Carol"); // ← ctor
return 0;
} // ← carol, alice の dtor(逆順)
今生きているオブジェクト: (なし)

最小コード

first_dtor.cpp最小例
class Greeter { std::string name_; public: Greeter(std::string n) : name_(std::move(n)) { std::cout << "Hello, " << name_ << "\n"; } ~Greeter() { // ← デストラクタ std::cout << "Bye, " << name_ << "\n"; } }; int main() { Greeter g{"World"}; // Hello, World ← ここで ctor } // Bye, World ← ここで dtor(自動)
ここまでで覚えること(3 つ):
  • デストラクタは ~ClassName()、引数・戻り値なし
  • スコープ終了で自動に呼ばれる(freefclose を手で書かなくていい)
  • 複数のオブジェクトは作った逆順で破棄される

よくある素朴な疑問

Q. 明示的に破棄したいときは?
→ スコープを切る({ ... } ブロックで囲む)のが定石。ヒープ割り当てなら delete(スマートポインタ推奨)、手動 dtor 呼び出しはしないのが鉄則。

Q. 例外が起きても呼ばれる?
呼ばれます。 これが RAII の強みで、try/catch を書かなくてもリソースが漏れません(スタック巻き戻し)。

Q. デストラクタ書かないとどうなる?
→ コンパイラが暗黙のデストラクタを用意する。メンバ変数(vector や string など)は自分でデストラクタを持つので連鎖的に破棄される。なので大多数のクラスは自作デストラクタ不要です。

2. デストラクタの基本

構文基本
class Foo { public: ~Foo() { // 名前はクラス名に ~ を付ける // 終了処理 } }; // 特徴: // ・引数なし(オーバーロード不可) // ・戻り値なし // ・1 つのクラスに 1 つだけ
呼ばれるタイミング自動
// ① ローカル変数: スコープ終了時 void f() { Foo a; } // ← ここで a.~Foo() // ② new したオブジェクト: delete 時 Foo* p = new Foo(); delete p; // ← ここで ~Foo() // ③ コンテナの要素: コンテナ自身が管理 std::vector<Foo> v; v.push_back(Foo{}); // v が壊れるとき要素もすべて ~Foo()

暗黙のデストラクタ

何も書かなければコンパイラが空のデストラクタを生成します(各メンバを自動で破棄)。あなた自身のクラスでも、中身が vector / string / unique_ptr のようなメンバだけならデストラクタを書く必要はありません

Rule of Zero: 自作のリソース管理をせず、STL 型をメンバとして使うだけなら、デストラクタ・コピー・ムーブのすべてがコンパイラ任せで安全。現代 C++ の最良の設計原則です(詳細は第 29 回)。

3. RAII ― C++ 最大の発明

RAII (Resource Acquisition Is Initialization) = 「資源の取得は初期化で、解放はデストラクタで」。対になる処理をに閉じ込めて、忘れる・順序ミスを言語機構で防ぎます。

C の手動管理忘れる・失敗する
FILE* fp = fopen("data.txt", "r"); if (!fp) return; // 処理... if (error) { fclose(fp); // ← 忘れがち return; } // 処理... fclose(fp); // ← すべての return で書く必要
エラー経路すべてで fclose が必要
C++ の RAII自動・安全
std::ifstream file{"data.txt"}; if (!file) return; // 処理... if (error) return; // ← file は自動でクローズ // 処理... // スコープ終了で file.~ifstream() が自動で呼ばれ fclose 相当の処理
書き忘れ不能、例外でも安全

RAII の基本パターン

自作 RAIIテンプレ
class FileHandle { FILE* fp_; public: // 取得はコンストラクタ explicit FileHandle(const char* path, const char* mode) : fp_(fopen(path, mode)) { if (!fp_) throw std::runtime_error("open failed"); } // 解放はデストラクタ ~FileHandle() { if (fp_) fclose(fp_); } // 通常はコピー禁止(所有権を明確にするため) FileHandle(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete; FILE* get() const { return fp_; } }; void process() { FileHandle f{"data.txt", "r"}; // 処理... 例外が出ても fclose 相当は必ず走る } // ← スコープ終了で自動 close

STL の代表的な RAII 型

RAII の真価は「例外安全」: 例外が投げられてスタックが巻き戻されても、途中で作られたローカル変数のデストラクタは必ず呼ばれます。だから C の goto cleanup: パターンや「エラー処理の後始末」を書く必要がありません。これが C++ が採用している例外機構の本質。

4. 破棄の順序

複数のオブジェクトが破棄される順序は決まっています。予測できるからこそ安全に使えます。

ローカル変数 ― 作成と逆順

LIFOLast In First Out
void f() { Obj a; // ctor a Obj b; // ctor b Obj c; // ctor c } // dtor c → dtor b → dtor a (逆順)

クラスメンバ ― 宣言順(コンストラクト)/逆順(デストラクト)

メンバの順宣言順で生成、逆順で破棄
class Container { A a_; // 1. ctor a_ B b_; // 2. ctor b_ C c_; // 3. ctor c_ }; // 破棄時は c_ → b_ → a_ (逆順)

vector の要素 ― 末尾から破棄

vector は末尾から順に要素のデストラクタを呼びます。これはメモリアクセスの局所性を保つため。

ここまでで RAII の基本は OK
最後は実用的な RAII 型の例。応用が効くようになります。

5. 実戦:よくある RAII クラス

① std::lock_guard ― mutex の自動解放

スレッド安全並行処理
#include <mutex> std::mutex mtx; void safe_inc(int& counter) { std::lock_guard<std::mutex> lock{mtx}; // 取得 ++counter; } // ← スコープ終了で自動 unlock、例外でも安全

② ScopedTimer ― 時間計測

経過時間を自動で出力デバッグ用
class Timer { using clock = std::chrono::steady_clock; clock::time_point start_ = clock::now(); std::string label_; public: explicit Timer(std::string l) : label_(std::move(l)) {} ~Timer() { auto elapsed = clock::now() - start_; std::cout << label_ << ": " << std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count() << " ms\n"; } }; void my_function() { Timer t{"my_function"}; // 処理... } // 終了時に経過時間が勝手に出る

③ ScopeExit ― 任意の後始末を予約

defer 風汎用
template<class F> class ScopeExit { F f_; public: explicit ScopeExit(F f) : f_(std::move(f)) {} ~ScopeExit() { f_(); } }; // 使い方: void doit() { ScopeExit g{[]{ std::cout << "cleanup\n"; }}; // 処理... } // 終了時に cleanup が走る(例外でも)
モダン C++ のエッセンス: 「対になる処理(開く/閉じる、ロックする/解除する、開始する/終了する)」はすべて RAII でラップする。これが例外安全なコードを自然に書けてしまう秘訣です。
広告スペース

確認クイズ

デストラクタと RAII を 4 問で確認。

Q1. デストラクタについて正しいのは?

引数を受け取れる
複数定義できる(オーバーロード)
引数なし・戻り値なし・クラスに 1 つだけ
static にできる
デストラクタは引数を取れず、オーバーロードもできず、1 クラスに 1 つ。名前は ~ClassName()、戻り値なし。呼ばれるタイミングが決まっているので引数を渡しようがない、という理由。

Q2. 次のコードの出力として正しい順序は?
{
Logger a{"A"};
Logger b{"B"};
}

ctor A, ctor B, dtor A, dtor B
ctor A, ctor B, dtor A and B 同時
ctor A, ctor B, dtor B, dtor A
ctor B, ctor A, dtor A, dtor B
順序は不定
コンストラクタは書いた順、デストラクタは逆順(LIFO)。この規則があるから、B が A に依存しているような場合でも、B が A より先に壊されるので安全。

Q3. RAII の核心として最も正しいのは?

クラスの継承を必須にする
メモリ確保をヒープで行うこと
資源取得をコンストラクタ、解放をデストラクタに結び付ける
例外を投げない
RAII = Resource Acquisition Is Initialization。取得(ctor)と解放(dtor)を型に閉じ込めることで、後始末忘れや順序ミスを防ぎます。例外が飛んでもスコープ終了は起きるので、自動的に例外安全になる。

Q4. 次の関数で、例外が起きた場合にリソースが確実に解放されるのは?

void f() { FILE* fp = fopen("x","r"); /* 処理 */ fclose(fp); }
void f() { char* p = new char[100]; /* 処理 */ delete[] p; }
void f() { std::ifstream fs{"x"}; /* 処理 */ }
void f() { pthread_mutex_lock(&mtx); /* 処理 */ pthread_mutex_unlock(&mtx); }
①②④は、処理の途中で例外が投げられると fclose/delete/unlock に到達せずリソースリーク。③の std::ifstream は RAII で、スコープ終了時に自動でクローズ(例外でも)。①は std::ifstream、②は std::unique_ptr、④は std::lock_guard に置き換えるのがモダン流儀。