Menu
Coddy logo textTech

パターンとしてのRAII

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

RAII(Resource Acquisition Is Initialization)は単なる C++ のイディオムではなく、リソース管理をオブジェクトのライフタイムに結び付ける強力な設計パターンです。スマートポインターを使った RAII についてはすでに見てきましたが、このパターンはファイルハンドル、ネットワーク 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はこのパターンに従い、構築時にミューテックスを取得し、破棄時に解放することで、unlockのし忘れによるデッドロックを防ぎます。

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

challenge icon

チャレンジ

簡単

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

コードを3つのファイルに分けて整理します。

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

    ConnectionPool classでは、利用可能な接続数を追跡します(constructorに渡されたcapacityから開始します)。次を実装します。

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

    ConnectionGuard classはRAIIパターンを体現します。次のようにします。

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

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

  • main.cpp:スコープを通じたRAIIの自動クリーンアップを実演します。

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

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

    指定されたcapacityでConnectionPoolをCreateします。次に、ネストされたスコープ(curly bracesを使用)内で、要求された数のConnectionGuardオブジェクトをCreateし、vectorに格納します。各ガードについて、接続に成功したかどうかを出力します。

    • 接続に成功した場合:Guard N: Connected
    • 接続できなかった場合:Guard N: Failed to connect

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

    スコープがendsした後(ガードが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

ガードがスコープ外に出ると接続がautomatically releaseされることに注目してください。mainコードで明示的にreleaseを呼び出すことはありません。DestructorはCreateされた順序と逆の順序で実行されます(最後のガードが最初にdestroyedされます)。そして、acquireされたすべての接続が必ず返されます。これがRAIIの力です。スコープがどのように終了しても、リソースのクリーンアップが自動的かつ確実に行われます。

自分で試してみよう

#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++オンラインコンパイラ