RAIIResource Acquisition Is InitializationC++DC便

RAII寿RAII (Sutter 1999)

典型的な用法

編集

RAII (smart pointer) C++newnew[]deletedelete[]C++寿C++std::vectornew[]delete[]RAII

RAIICfopen()FILEfclose()C++C++寿C++03Boost C++Microsoft Foundation ClassRAIIC++11RAIIstd::lock_guardstd::unique_lock

RAIIC++03C++std::auto_ptrC++11std::unique_ptrBoost C++boost::scoped_ptrboost::interprocess::unique_ptrBoost C++boost::shared_ptrC++11std::shared_ptrshared_ptrboost::weak_ptrstd::weak_ptrboost::intrusive_ptrLokiLoki::SmartPtrCOM (IUnknown) ATLATL::CComPtr

RAIIRAII使try-catch

C++での例

編集

以下、特に断りがない限り、C++03以前でもC++11以降でもコンパイルできるコードで例示する。

動的確保されたメモリの管理

編集

単純な例として、関数内で一時的な作業領域として配列を動的確保することを考える[1]。単純な方法では、以下のようにnew[]演算子を使用する。

void function1A(size_t count) {
    double* array1 = NULL;
    double* array2 = NULL;
    try {
        // 配列を動的に確保する。メモリ確保失敗により std::bad_alloc 例外がスローされる可能性がある。
        array1 = new double[count]();
        array2 = new double[count]();

        // 動的に確保した配列をここで作業領域として使用する。
        for (size_t i = 0; i < count; ++i) {
            array1[i] = i * 0.1;
            array2[i] = i * 0.1;
        }
        // ...

        // 配列を使わなくなったので削除する。
        delete[] array2;
        delete[] array1;
    }
    catch (...) {
        // 例外がスローされる場合に備える。
        delete[] array2;
        delete[] array1;
        throw; // 例外の再送出。
    }
}

これはC言語のmallocおよびfree関数による原始的な寿命管理手法に近い。もし動的に確保したメモリを削除する前に関数を抜けるとメモリリークしてしまうため、慎重に削除処理をひとつひとつ記述していく必要がある。動的にメモリ管理するオブジェクトの数が増えるにつれ、ソースコードのメンテナンスコストは増大していく。

一方、RAIIを利用した場合は以下のようになる。

// RAII を実現する配列ラッパークラス。
template<typename T> class ArrayWrapper {
    size_t m_count;
    T* m_data;
public:
    ArrayWrapper() : m_count(), m_data() {}
    explicit ArrayWrapper(size_t count) : m_count(count), m_data(new T[count]()) {}
    ~ArrayWrapper() { delete[] m_data; }
    size_t count() const { return m_count; }
    T* data() { return m_data; }
    const T* data() const { return m_data; }
    T& operator[](size_t index) { return m_data[index]; }
    const T& operator[](size_t index) const { return m_data[index]; }
    // コピーは禁止とする。所有権の移動もサポートしない。
private:
    ArrayWrapper(const ArrayWrapper&);
    ArrayWrapper& operator=(const ArrayWrapper&);
};

void function1B(size_t count) {
    ArrayWrapper<double> array1(count);
    ArrayWrapper<double> array2(count);

    // 動的に確保した配列をここで作業領域として使用する。
    for (size_t i = 0; i < count; ++i) {
        array1[i] = i * 0.1;
        array2[i] = i * 0.1;
    }
    // ...

} // RAII 変数 array1, array2 の属するブロックを抜ける。このとき array2, array1 の各デストラクタが順に呼ばれ、それぞれが内部で管理する配列メモリ領域は自動的に破棄される。

Boost C++ライブラリboost::scoped_arrayを使う場合は以下のように書ける。

#include <boost/scoped_array.hpp>

void function1C(size_t count) {
    boost::scoped_array<double> array1(new double[count]());
    boost::scoped_array<double> array2(new double[count]());

    for (size_t i = 0; i < count; ++i) {
        array1[i] = i * 0.1;
        array2[i] = i * 0.1;
    }
}

C++11以降のstd::unique_ptrを使う場合は以下のように書ける。

#include <memory>

void function1D(size_t count) {
    std::unique_ptr<double[]> array1(new double[count]());
    std::unique_ptr<double[]> array2(new double[count]());

    for (size_t i = 0; i < count; ++i) {
        array1[i] = i * 0.1;
        array2[i] = i * 0.1;
    }
}

RAII使returnC++RAIIC++ (STL) RAIIstd::vector

コンストラクタからの例外送出とRAII

編集

コンストラクタの実行中、処理が最後まで完了する前に例外がスローされた場合、デストラクタが呼ばれない。そのため、コンストラクタで複数のリソースをnew/new[]してポインタ型のメンバー変数に格納し、デストラクタでdelete/delete[]するようなコードをうかつに書いてしまうとメモリリークの原因となる。

template<typename T> class DualArrayWrapper {
    size_t m_count;
    T* m_data1;
    T* m_data2;
public:
    explicit DualArrayWrapper(size_t count) : m_count(count), m_data1(), m_data2() {
        m_data1 = new T[count]();
        m_data2 = new T[count]();
        // m_data2 への代入右辺式が std::bad_alloc 例外をスローした場合、
        // DualArrayWrapper のデストラクタが呼ばれず、
        // m_data1 に割り当てたメモリがリークする。
    }
    ~DualArrayWrapper() {
        delete[] m_data2;
        delete[] m_data1;
    }
// 他のメンバーの実装は省略。
};

try-catch使RAII使
#include <vector>

template<typename T> class DualArrayWrapper {
    std::vector<T> m_data1;
    std::vector<T> m_data2;
public:
    explicit DualArrayWrapper(size_t count): m_data1(count), m_data2(count) {
        // m_data2 のコンストラクタが std::bad_alloc 例外をスローした場合、
        // DualArrayWrapper のデストラクタは呼ばれないが、
        // m_data1 のデストラクタは呼ばれるため、メモリリークしない。
    }
// デストラクタを明示的に記述する必要はなく、デフォルト生成されるもので十分となる。
};

単一のリソースを管理するクラスの場合は、デストラクタで明示的に解放することが許容される[2]

ファイルハンドルの管理

編集

別の例として、ファイルのオープンとクローズを挙げる。従来の標準Cライブラリを使って、直接リソースを管理する書き方だと以下のようになる。

#include <cstdio>
#include <cassert>
#include <stdexcept>

FILE* openFile(const char* fileName, const char* mode) {
    FILE* fp = std::fopen(fileName, mode);
    if (!fp) {
        throw std::runtime_error("Failed to open file!");
    }
    return fp;
}

void writeLine(FILE* fp, const char* strLine) {
    assert(fp);
    const int ret = std::fprintf(fp, "%s\n", strLine);
    if (ret < 0) {
        throw std::runtime_error("Failed to write data on file!");
    }
}

void function2A() {
    FILE* fp1 = NULL;
    FILE* fp2 = NULL;
    try {
        fp1 = openFile("test1.txt", "a");
        fp2 = openFile("test2.txt", "a");
        // ファイルの書き込みを行なう。
        writeLine(fp1, "Test line for file#1.");
        writeLine(fp2, "Test line for file#2.");
        // ファイルを使い続ける。
        // 何か問題が起こって関数を抜ける場合、return の前に fclose() を忘れずに呼ばなければならない。

        // 明示的にファイルを閉じる必要がある。
        std::fclose(fp1);
        std::fclose(fp2);
    }
    catch (...) {
        // 獲得したリソースがあれば返却する。
        if (fp1) {
            std::fclose(fp1);
        }
        if (fp2) {
            std::fclose(fp2);
        }
        throw; // 例外の再送出。
    }
}

一方、RAIIを利用した場合は以下のようになる。

class FileWrapper {
    FILE* m_fp;
public:
    FileWrapper(const char* fileName, const char* mode)
        : m_fp(std::fopen(fileName, mode)) { // ファイルハンドルでデータメンバーを初期化。
        if (!m_fp) {
            throw std::runtime_error("Failed to open file!");
        }
    }
    ~FileWrapper() {
        assert(m_fp);
        std::fclose(m_fp);
    }
    void writeLine(const char* strLine) {
        assert(m_fp);
        const int ret = std::fprintf(m_fp, "%s\n", strLine);
        if (ret < 0) {
            throw std::runtime_error("Failed to write data on file!");
        }
    }
    // コピーは禁止とする。所有権の移動もサポートしない。
private:
    FileWrapper(const FileWrapper&);
    FileWrapper& operator=(const FileWrapper&);
};

void function2B() {
    FileWrapper file1("test1.txt", "a");
    file1.writeLine("Test line for file#1.");
    FileWrapper file2("test2.txt", "a");
    file2.writeLine("Test line for file#2.");
} // 関数の途中で return したり、例外がスローされたりしても、RAII 変数の属するブロックを抜けた時点で確実にファイルハンドルは閉じられる。

C++RAIIstd::basic_fstream

FileWrapperFILE*RAIIRAIIFileWrapper

RAII使catchfunction1Afunction2Atry-catchfunction1Bfunction2BRAIIRAIIRAII使

function1Afunction2AJavaRAII使Javatry-finallytry-finally

広義のスマートポインタの活用

編集

使RAIIAPI

使

stlsoft::scoped_handle[3]void0Windows

Windows APIWinsock APIRAIICreateFile()[4]INVALID_HANDLE_VALUEWSASocket()[5]INVALID_SOCKETWindows SDK 8.1
#define INVALID_HANDLE_VALUE  ((HANDLE)((LONG_PTR)-1))
#define INVALID_SOCKET  (SOCKET)(~0)

RAIIは特に複数のリソースを同時に管理する場合に効果を発揮する。少なくともtry-catch節がいくつも現れて混乱する事態からは逃れられる。

#include <WinSock2.h>
#include <cstdlib>
#include <cassert>
#include <cstring>
#include <stdexcept>
#include <iostream>
#include <stlsoft/smartptr/scoped_handle.hpp>

// 3つの資源を同時に使う。
void testScopedHandle() {
    // ファイルを開く。
    // CreateFile() は失敗した場合 INVALID_HANDLE_VALUE を返す。ただし NULL もまた HANDLE としては一般的に無効値。
    HANDLE hFile = ::CreateFileW(L"test.txt", GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == NULL || hFile == INVALID_HANDLE_VALUE) {
        throw std::runtime_error("Failed to create file handle!");
    }
    else {
        stlsoft::scoped_handle<HANDLE> cleanupFile(hFile, ::CloseHandle, INVALID_HANDLE_VALUE); // ファイルが確実に閉じられるようにする。

        // TCP ソケットを作成。
        // BSD ソケット API における socket() 関数の戻り値は int で、異常値は負数 (-1) となっており、0 は正常値のひとつ。
        // Winsock もそれを踏襲している。
        SOCKET socketDesc = ::WSASocketW(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
        if (socketDesc == INVALID_SOCKET) {
            throw std::runtime_error("Failed to create socket descriptor!");
        }
        else {
            stlsoft::scoped_handle<SOCKET> cleanupSocket(socketDesc, ::closesocket, INVALID_SOCKET); // ソケットが確実に閉じられるようにする。

            void *mem = std::malloc(10000);
            if (!mem) {
                throw std::bad_alloc();
            }
            else {
                stlsoft::scoped_handle<void*> cleanupMem(mem, std::free); // メモリが確実に解放されるようにする。

                // ここでメモリとソケットとファイルを使う。

                const LARGE_INTEGER dummy = {};
                if (!SetFilePointerEx(cleanupFile.get(), dummy, NULL, FILE_END)) {
                    throw std::runtime_error("Failed to set file pointer to end!");
                }

                const char* text = "Test line.\r\n";
                const DWORD numOfBytesToWrite = static_cast<DWORD>(std::strlen(text));
                DWORD numOfBytesWritten = 0;
                if (!::WriteFile(cleanupFile.get(), text, numOfBytesToWrite, &numOfBytesWritten, NULL) || numOfBytesWritten != numOfBytesToWrite) {
                    throw std::runtime_error("Failed to write data on file!");
                }

                // ...

            } // mem はここで解放される。
            mem = NULL;

            // ソケットを自動的な管理から切り離す。
            //SOCKET detachedVal = cleanupSocket.detach();
            //assert(detachedVal == socketDesc);

        } // socketDesc を RAII から切り離した場合、ここでは閉じられない。

        //const int ecode = ::closesocket(socketDesc);
        socketDesc = INVALID_SOCKET;

        // 早期に hFile の資源を返却することもできる。
        //cleanupFile.close();

    } // hFile の資源を早期に返却した場合、ここでは返却されない。
    hFile = INVALID_HANDLE_VALUE;
}

int main() {
    WSADATA wsaData = {};
    const int ecode = ::WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (ecode == 0) {
        try {
            testScopedHandle();
        }
        catch (const std::exception& ex) {
            std::cout << ex.what() << std::endl;
        }
        ::WSACleanup();
    }
}

制約

編集

RAIIクラスでは解放関数が失敗すると問題になる。C++では言語の制約上デストラクタから例外を投げるのは良い考えではないため、デストラクタではすべての例外を握りつぶす必要がある。エラーコードによる通知も難しくなるため、結果として解放失敗の原因を上位層に通知することが難しくなる。そのためstlsoft::scoped_handleのようなクラスは、次のどちらかに当てはまるときには使うべきではない。

  1. 解放関数が失敗する可能性のある場合
  2. 利用者がその失敗を知るべき場合

クロージャとRAII

編集

RubySmalltalkは特別なスコープに関連付けられた変数の中にあるクロージャブロックという形でRAIIに対応している。以下はRubyの例である。

File.open("data.txt") { |file|
    # ファイルの内容を標準出力へ
    print file.read
}
# 変数'file'はもう存在しない。ファイルハンドルは閉じられた。

RAIIに類似した制御構造

編集

C#VB .NET 2005はC++デストラクタに代わるSystem.IDisposableインターフェイスを実装するクラスとusing文を使ってRAIIに似た機能を実現している。

Python 2.5に追加されたwithステートメントでは、同様の目的に__enter____exit__のメソッドを使う。

Javaはバージョン7で導入されたtry-with-resources文により、C#のusing文に近い機能を提供する。

脚注

編集


(一)^ gccC99mallocnew

(二)^ How to: Design for exception safety | Microsoft Learn

(三)^ STLSoft: scoped_handle Class Template Reference

(四)^ CreateFileW function | Microsoft Docs

(五)^ WSASocketW function | Microsoft Docs

参考文献

編集
  • Sutter, Herb (1999). Exceptional C++. Addison-Wesley. ISBN 978-0-201-61562-3.
    • 日本語訳 ハーブ・サッター 『Exceptional C++』 浜田真理、ピアソンエデュケーション、2000年、249頁。ISBN 978-4-89471-270-6

外部リンク

編集