📗

Janitor(清掃員)とRAIIの実装例

に公開

概要

ゲーム開発では、動的なメモリ管理やリソース管理の煩雑さがバグの原因になることが多くあります。こうした問題を軽減する手法の一つが、リソースの確保を初期化時に行うRAII(Resource Acquisition Is Initialization) です。

このRAIIのアイデアを体現した具体的なC++クラスとして、「Janitor(清掃員)」があります。リソースの確保と管理は、それぞれクラスのコンストラクタとデストラクタで行われます。プログラマがさまざまな処理を行ったあと、このクラスが「片付けて」くれることから、Janitor(清掃員)と呼ばれています。

本題

RAIIの原則とは、「リソースの確保をオブジェクトの初期化時に、解放を破棄時に行う」ことです。
JanitorクラスはこのRAIIを応用し、アロケータコンテキストのPush/Popを自動化する例です。

ファイル構成

SimpleRAIIProject/
├── include/
│   └── mem/
│       └── Allocator.hpp     // コンテキストのPush/Pop
├── src/
│   ├── AllocJanitor.hpp      // Janitorクラス
│   └── main.cpp              // 使用例

Allocator.hpp — グローバルなコンテキスト管理

#pragma once
#include <stack>
#include <string>
#include <iostream>

namespace mem
{
    using Context = std::string;

    inline std::stack<Context>& allocatorStack() {
        static std::stack<Context> stack;
        return stack;
    }

    inline void PushAllocator(const Context& ctx) {
        allocatorStack().push(ctx);
        std::cout << "Push: " << ctx << "\n";
    }

    inline void PopAllocator() {
        if (!allocatorStack().empty()) {
            std::cout << "Pop: " << allocatorStack().top() << "\n";
            allocatorStack().pop();
        } else {
            std::cerr << "Warning: PopAllocator called on empty stack!\n";
        }
    }

    inline const Context& GetCurrentAllocator() {
        return allocatorStack().top();
    }
}

AllocJanitor.hpp — RAIIを実現するJanitorクラス

#pragma once
#include "mem/Allocator.hpp"

class AllocJanitor {
public:
    explicit AllocJanitor(mem::Context context) {
        mem::PushAllocator(context);
    }

    ~AllocJanitor() {
        mem::PopAllocator();
    }

    AllocJanitor(const AllocJanitor&) = delete;
    AllocJanitor& operator=(const AllocJanitor&) = delete;
};

main.cpp — 使用例

#include "AllocJanitor.hpp"
#include <iostream>

void DoSomething() {
    AllocJanitor janitor("TemporaryContext");
    std::cout << "Doing something in " << mem::GetCurrentAllocator() << "...\n";
} // janitor がスコープを抜けると自動で Pop される

int main() {
    mem::PushAllocator("MainContext");

    {
        DoSomething(); // スコープ内で TemporaryContext を一時的に使用
    }

    mem::PopAllocator(); // MainContext を手動でポップ(RAIIで管理してもよい)

    return 0;
}

実行例

$ cd /path/to/SimpleRAIIProject
$ g++ -std=c++20 -Iinclude src/main.cpp -o janitor_example
$ ./janitor_example

実行結果

Push: MainContext
Push: TemporaryContext
Doing something in TemporaryContext...
Pop: TemporaryContext
Pop: MainContext

参考文献

Jason Gregory. ゲームエンジン・アーキテクチャ 第3版. ボーンデジタル. 2020

Discussion