リソースの解放忘れを防ぐには? Effective C++ 14項を読んで

Scott Mayers著, 小林健一郎訳, Effective C++ 第3版, 丸善出版(2014) を1日1項目読んでいます。

今日は14項を読みました。

リソース(メモリ、ファイルディスクリプタミューテックスロック、DB接続、ネットワークソケットなど)の解放忘れを防ぐにはどうしたらよいでしょうか? RAIIオブジェクトをリソースを渡すことで生成すればよい。そして、RAIIオブジェクトとしては、C++11のstd::shared_ptrを使うとよい、ということがわかりました。

どうして、リソースの解放忘れが起こるのでしょうか? それは、例えば、リソースとしてメモリを考えると、ある関数内でヒープ領域にメモリを確保すると、その関数を抜ける前に、メモリを解放する必要があります。

void f(){
    A* a = new A();
   ... // ここで、returnしたり、例外が発生したりするとdeleteが実行されない。
   delete a;
}

しかし、上のように、deleteする前にreturnしたり、例外が発生したりするとdeleteが実行されず、メモリリークになります。何らかの理由で関数を抜ける際に、必ずdeleteが実行されるような仕組みはないでしょうか? これを解決するアイディアはデストラクタです。

デストラクタは、そのオブジェクトの寿命が終わると実行されます。つまり、関数内で、あるオブジェクトをポインタなどのリソースを渡すことで生成すれば、関数を抜けるときに、オブジェクトのデストラクタが実行されるので、このデストラクタでポインタをdeleteしてもらうことができます。

このデストラクタをもつオブジェクトのことをRAIIオブジェクトといいます。既に用意されているRAIIオブジェクトとして、C++11のstd::shared_ptrがあります。また、std::shared_ptrをstaticでないメンバとしてもつオブジェクトもRAIIオブジェクトになります。なぜなら、デストラクタが呼ばれるとき、メンバのデストラクタも呼ばれるからです。

リソースがメモリのときには、リソースへのポインタをstd::shared_ptrのコンストラクタに渡します。すると、std::shared_ptrのデストラクタでこのポインタがdeleteされます。(delete[]でないことに注意)

リソースがメモリでないときには、リソースへのポインタをdeleteするとともに、何らかの解放処理(デリータ)が必要になります。リソースへのポインタを引数とする関数オブジェクト、すなわちデリータをstd::shared_ptrのコンストラクタに渡せます。

リソースがミューテックスロックの場合のサンプル

#include <memory>
#include <iostream>

class Mutex {
    public:
    void lock(){
        std::cout << "locked\n";
    }

    void unlock(){
        std::cout << "unlocked\n";
    }
};

void lock(Mutex* pm){
    pm->lock();
}

void unlock(Mutex* pm){
    pm->unlock();
}

class Lock {
    public:
    Lock(Mutex* pm)
    : mutexPtr(pm, unlock)
    {
        lock(mutexPtr.get());
    }

    ~Lock(){}

    private:
    std::shared_ptr<Mutex> mutexPtr;
};

void test(const Lock& ml){
    std::cout << "test start\n";
    {
        Lock ml2(ml);
        std::cout << "in critical session in test...\n";
    }
    std::cout << "test end\n";
}

int main(int argc, char* argv[]){
    Mutex m;
    std::cout << "start\n";
    {
        Lock ml1(&m);
        std::cout << "in critical session...\n";
        test(ml1);
    }
    std::cout << "end\n";

    return 0;
}

コンパイル

$ clang++ -std=c++11 Lock.cpp -o lock

実行

$ ./lock
start
locked
in critical session...
test start
in critical session in test...
test end
unlocked
end

Scott Mayersさんのブログhttp://scottmeyers.blogspot.jp/も読んでみようと思います。