Menu
Coddy logo textTech

パターンとしてのRAII

CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 102/104。

RAII (Resource Acquisition Is Initialization)は単なる C++ のイディオムではありません。リソース管理をオブジェクトのライフタイムに結び付ける、強力なデザインパターンです。スマートポインターで RAII をすでに見てきましたが、このパターンはあらゆるリソースに適用できます。たとえば、file ハンドル、ネットワーク connections、ミューテックス、データベーストランザクションなどです。

基本的な考え方は単純です。constructor でリソースを acquire し、デストラクタで解放します。C++ ではオブジェクトがスコープ外に出るとデストラクタが実行されることが保証されているため、例外が発生した場合でもクリーンアップが自動的に行われます。

#include <iostream>
#include <fstream>

class FileGuard {
    std::ofstream file;
public:
    FileGuard(const std::string& filename) : file(filename) {
        if (!file.is_open()) {
            std::cout << "Failed to open file\n";
        }
    }
    
    void write(const std::string& text) {
        if (file.is_open()) file << text;
    }
    
    ~FileGuard() {
        if (file.is_open()) {
            file.close();
            std::cout << "File closed automatically\n";
        }
    }
};

int main() {
    {
        FileGuard guard("output.txt");
        guard.write("Hello RAII");
    }  // ここでデストラクタが呼び出される - ファイルが閉じられる
    
    std::cout << "After scope\n";
}

RAIIは、マルチスレッドコードでロックを管理する際に力を発揮します。標準ライブラリのstd::lock_guardはこのパターンに従っており、構築時にミューテックスをacquireし、破棄時に解放することで、unlockのし忘れによるデッドロックを防ぎます。

RAII クラスを実装する際は、リソースの重複や二重解放の問題を防ぐために、copy/move 操作を delete するか、適切に実装すること(Rule of Five)を忘れないでください。RAII によって、エラーが発生しやすい手動のリソース管理が、安全で自動的なクリーンアップに変わります。

challenge icon

チャレンジ

簡単

RAIIを使用してデータベース接続を安全に管理するConnection Pool Managerを構築しましょう。実際のアプリケーションでは、データベース接続は高コストなリソースであり、適切にacquireしてreleaseする必要があります。例外が発生した場合やコードパスが複雑になった場合でも、接続が常にプールへ返されることを保証するRAIIラッパーを作成します。

コードを3つのFileに分けて整理します。

  • ConnectionPool.h:限られた数の接続を管理するシンプルな接続プールをCreateします。

    ConnectionPool classでは、availableな接続数を追跡します(constructorに渡されたcapacityから開始します)。以下を実装してください。

    • acquire():接続がavailableな場合はcountを減らし、残りのcountを示すConnection acquired (X available)を出力します。成功した場合はtrue、接続がavailableでない場合はfalseを返します。
    • release():availableなcountを増やし、Connection released (X available)を出力します。
    • available():現在availableな接続数を返します。
  • ConnectionGuard.h:単一の接続を安全に管理するRAIIラッパーを構築します。

    ConnectionGuard classはRAIIパターンを具体化します。以下のようにしてください。

    • constructorでConnectionPoolへの参照を受け取り、接続のacquireを試みる
    • acquisitionが成功したかどうかを保持する
    • guardが有効な接続を保持しているか確認するためのisConnected()メソッドを提供する
    • Destructorで、acquireされた接続がある場合にのみ、接続を自動的にプールへreleaseする
    • リソースの重複を防ぐため、copy constructorとcopy assignmentをDeleteする(Rule of Fiveに関する考慮)

    Destructorが実行されたときに接続を保持していた場合は、プールでreleaseを呼び出す前にGuard releasing connectionを出力します。

  • main.cpp:scopeを通じたRAIIの自動cleanupを示します。

    2つの入力を読み取ります。

    1. プールのcapacity(整数)
    2. requestする接続数(整数)

    指定されたcapacityでConnectionPoolをCreateします。次に、nested scope(curly bracesを使用)の中で、requestされた数のConnectionGuardオブジェクトをvectorに格納してCreateします。各guardについて、接続に成功したかどうかを出力します。

    • 接続された場合:Guard N: Connected
    • 接続されなかった場合:Guard N: Failed to connect

    (Nは1から開始します)

    scopeがendsした後(guardsがdestroyedされた後)、After scope: X connections availableを出力して、プールの最終状態を示します。

たとえば、入力が23の場合:

Connection acquired (1 available)
Guard 1: Connected
Connection acquired (0 available)
Guard 2: Connected
Guard 3: Failed to connect
Guard releasing connection
Connection released (1 available)
Guard releasing connection
Connection released (2 available)
After scope: 2 connections available

入力が32の場合:

Connection acquired (2 available)
Guard 1: Connected
Connection acquired (1 available)
Guard 2: Connected
Guard releasing connection
Connection released (2 available)
Guard releasing connection
Connection released (3 available)
After scope: 3 connections available

guardsがscope外に出ると、接続が自動的にreleaseされることに注目してください。mainのコードでreleaseを明示的に呼び出すことはありません。Destructorsはconstructionの逆順(最後のguardが最初にdestroyed)で実行され、acquiredされたすべての接続が必ず返されます。これがRAIIの力です。scopeがどのように終了しても、リソースのcleanupが自動的かつ確実に行われます。

自分で試してみよう

#include <iostream>
#include <vector>
#include "ConnectionPool.h"
#include "ConnectionGuard.h"

using namespace std;

int main() {
    // 入力を読み取る
    int capacity;
    int numConnections;
    cin >> capacity;
    cin >> numConnections;
    
    // TODO: 指定された容量で ConnectionPool を作成する
    
    // TODO: 中括弧を使ってネストしたスコープを作成する
    {
        // TODO: ConnectionGuard オブジェクトを格納する vector を作成する
        // Hint: ポインタまたはスマートポインタを使用する必要があります。ConnectionGuard は
        // コピーコンストラクタが削除されています
        
        // TODO: numConnections 個のガードを作成するループ
        // 各ガードについて、次のいずれかを出力します:
        // "Guard N: Connected" or "Guard N: Failed to connect"
        // ここで N は 1 から始まります
        
    }
    // スコープが終了すると、ここでガードが破棄されます
    
    // TODO: Print "After scope: X connections available"
    
    return 0;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: C++オンラインコンパイラ