Menu
Coddy logo textTech

パターンとしてのRAII

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

RAII (Resource Acquisition Is Initialization)は、単なるC++のイディオムではありません。それは、リソース管理をオブジェクトの生存期間(ライフタイム)に結び付ける強力なデザインパターンです。すでにスマートポインタでRAIIを目にしているかもしれませんが、このパターンはファイルハンドル、ネットワーク接続、ミューテックス、データベーストランザクションなど、あらゆるリソースに適用されます。

基本的な考え方は単純です:コンストラクタでリソースを取得し、デストラクタでそれらを解放します。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 はこのパターンに従っています。これは構築時にミューテックスを取得し、破棄時にそれを解放することで、アンロックの忘れによるデッドロックを防ぎます。

RAIIクラスを実装する際は、リソースの重複や二重解放の問題を防ぐために、コピー/ムーブ操作(Rule of Five:五則)を削除するか、適切に実装することを忘れないでください。RAIIは、エラーが発生しやすい手動のリソース管理を、安全で自動的なクリーンアップへと変換します。

challenge icon

チャレンジ

簡単

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

コードは以下の3つのファイルに分けて構成します:

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

    ConnectionPoolクラスは、利用可能な接続数を追跡する必要があります(コンストラクタに渡された容量から開始します)。以下を実装してください:

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

    ConnectionGuardクラスはRAIIパターンを体現します。以下の機能を持たせてください:

    • コンストラクタでConnectionPoolへの参照を受け取り、接続の取得を試みる
    • 取得が成功したかどうかを保存する
    • ガードが有効な接続を保持しているか確認するためのisConnected()メソッドを提供する
    • デストラクタで、接続が取得されていた場合にのみ、自動的に接続をプールに解放する
    • リソースの重複を防ぐため、コピーコンストラクタとコピー代入演算子を削除する(Rule of Fiveの考慮)

    デストラクタが実行される際、接続を保持していた場合は、プールのreleaseを呼び出す前にGuard releasing connectionと出力してください。

  • main.cpp: スコープによるRAIIの自動クリーンアップを実証します。

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

    1. プールの容量(整数)
    2. リクエストする接続数(整数)

    指定された容量でConnectionPoolを作成します。次に、入れ子になったスコープ(波括弧を使用)内で、リクエストされた数のConnectionGuardオブジェクトを作成し、ベクトルに格納します。各ガードについて、接続に成功したかどうかを出力します:

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

    (Nは1から始まります)

    スコープが終了した後(ガードが破棄された後)、プールの最終状態を示す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

ガードがスコープ外に出ると、接続が自動的に解放されることに注目してください。メインコードで明示的にreleaseを呼び出す必要はありません。デストラクタは構築の逆順(最後に作成されたガードが最初に破棄される)で実行され、取得されたすべての接続が確実に返却されることが保証されます。これがRAIIの力です。スコープをどのように抜けても、リソースのクリーンアップが自動的かつ確実に行われます。

チートシート

RAII(Resource Acquisition Is Initialization:リソースの確保は初期化時に)は、リソース管理をオブジェクトの生存期間に結び付けるデザインパターンです。リソースはコンストラクタで確保され、デストラクタで解放されます。

C++ではオブジェクトがスコープを抜ける際にデストラクタが実行されることが保証されているため、例外が発生した場合でもクリーンアップが自動的に行われます。

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は、構築時にミューテックスを確保し、破棄時に解放することで、アンロックの忘れによるデッドロックを防ぎます。

RAIIクラスを実装する際は、リソースの重複や二重解放の問題を防ぐために、コピー/ムーブ操作を削除するか、適切に実装してください(Rule of Five:五則)。

自分で試してみよう

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

using namespace std;

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

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

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