【C++】RAIIとは?|リソース管理の基本思想をゲーム開発で理解する

C++のRAIIのイメージ|リソース管理の基本思想 C++

確保したら、解放する。ファイルを開いたら、閉じる。そのたびに後始末を書いていると、書き忘れや、途中でのreturnで、リソースが漏れます。RAIIは、この後始末を、オブジェクトに任せる考え方です。

名前は難しそうですが、中身は、「使い始めるときに用意して、スコープを抜けたら自動で片づける」というだけです。この記事では、RAIIの意味、必要な理由、書き方、ゲーム開発での使いどころ、初心者がハマりやすいミスを解説します。

RAIIとは?|名前の意味

RAIIは Resource Acquisition Is Initialization の略で、日本語にすると「リソースの確保=初期化」です。少し意訳すると、「リソースの寿命を、オブジェクトの寿命に結びつける」という意味になります。

  • コンストラクタでリソースを確保する
  • デストラクタでリソースを解放する
  • スコープを抜けるとデストラクタが自動で呼ばれるので、解放し忘れが起きない

ここでいう「リソース」は、メモリだけではありません。ファイル・ロック・テクスチャ・サウンド・ネットワーク接続など、「使ったら必ず後始末が必要なもの」すべてが対象です。仕組みの土台はデストラクタなので、コンストラクタとデストラクタの記事も合わせて読むと理解が深まります。

なぜRAIIが必要なのか

手動で後始末をするコードは、解放を書き忘れたり、途中でreturnしてすり抜けたりして、簡単にリソース漏れを起こします。

void LoadStage() {
    FILE* fp = fopen("stage.dat", "rb");
    if (!ReadHeader(fp)) {
        return; // ★ fclose を忘れてリソース漏れ!
    }
    // ...読み込み処理...
    fclose(fp);
}

途中の return で fclose を飛ばしてしまいました。こういう「うっかり」を、仕組みで根絶するのがRAIIです。

RAIIを自分で書いてみる

ファイルを「確保したら必ず閉じる」クラスにしてみましょう。

class FileHandle {
public:
    // 確保 = 初期化
    explicit FileHandle(const char* path) {
        fp_ = fopen(path, "rb");
    }
    // 解放 = デストラクタ
    ~FileHandle() {
        if (fp_) fclose(fp_);
    }
    FILE* Get() const { return fp_; }

private:
    FILE* fp_ = nullptr;
};

void LoadStage() {
    FileHandle file("stage.dat");
    if (!ReadHeader(file.Get())) {
        return; // ★ ここで抜けても自動で fclose される
    }
    // ...読み込み処理...
} // ★ 関数を抜けた瞬間、~FileHandle() が呼ばれる

どこで return しても、例外が飛んでも、スコープを抜ければ必ずデストラクタが呼ばれて閉じられます。もう「閉じ忘れ」を心配する必要がありません。

身近なRAIIの例|実は毎日使っている

難しく考えなくても、あなたはすでにRAIIを使っています。代表格がスマートポインタです。

  • std::unique_ptr … スコープを抜けると自動で delete
  • std::vector … 寿命が尽きると確保したメモリを自動で解放
  • std::lock_guard … スコープを抜けると自動でロック解除

これらは全部「RAIIで作られた道具」です。つまりRAIIを理解することは、標準ライブラリがなぜ安全なのかを理解することでもあります。

【重要】私がRAIIのありがたみを痛感した体験談

自主制作のゲームで、テクスチャの読み込みと解放を手動で管理していた頃、シーンを切り替えるたびにメモリがじわじわ増え続けるという不具合に悩まされました。

原因は、あるエラー処理の分岐でテクスチャの解放を1か所だけ書き忘れていたこと。分岐が増えるほど、こういう漏れは必ず出ます。そこでテクスチャを「確保したら必ず解放する」RAIIクラスに包んだところ、解放処理をコードから全部消せて、リークがピタッと止まりました。

「後始末を人間が覚えておく」のをやめて、オブジェクトの寿命に任せる。これがどれだけ楽で安全か、身をもって実感した出来事でした。

RAIIのよくある失敗例と対処法

①コピーして二重解放

RAIIクラスを何気なくコピーすると、同じリソースを2つのオブジェクトが持ち、両方がデストラクタで解放してクラッシュします。コピーを禁止するか、ムーブだけ許可するのが基本です。

②デストラクタで例外を投げる

デストラクタから例外を投げると、プログラムが即座に終了することがあります。後始末の中でエラーが出ても、デストラクタの外に例外を漏らさないようにします。

③new したポインタを生のまま持つ

せっかくRAIIを学んでも、生ポインタで new していては意味がありません。まずは unique_ptr に置き換えるだけで、多くのリークが消えます。

注意点

  • RAIIクラスはコピーの扱いを必ず決める(禁止 or ムーブのみ)
  • デストラクタから例外を外に漏らさない
  • 自作する前に、まず標準の unique_ptr / vector で済まないか考える

ここまでのおさらい

  • RAIIはリソースの寿命をオブジェクトの寿命に結びつける考え方
  • 確保はコンストラクタ、解放はデストラクタで自動化する
  • スマートポインタやvectorもRAIIで作られた道具
  • ハマりどころはコピーによる二重解放

RAIIでは、確保をコンストラクタで、解放をデストラクタで自動化します。スマートポインタやvectorも、RAIIで作られた道具です。ハマりどころは、コピーによる二重解放です。

関連記事