ゆとりーなの日記

日記的な事を書いて行くと思はれる

std::unique_ptrとstd::shared_ptr

なんかVC++2010だとstd::unique_ptrが使えたのでメモメモ。
std::unique_ptrといえば、boost::scoped_ptrがなんか知らないけど名前が変わっちゃったものという認識がありましたがstd::unique_ptrの方はカスタムデリータが使用出来るみたいです。std::shared_ptrだとオーバースペックだからboost::scoped_ptr使いたいけどカスタムデリータ使えないから不便だなー的なものが解消されました。
カスタムデリータはdelete以外の方法でリソースを解放するポインタをスマートポインタに入れるのに使うわけです。自分で専用のRAIIオブジェクト作ればいーじゃん的な話もあると思いますが、スマートポインタは結構慎重に作らないとすぐにアホなポインタが出来上がってしまうので、やはりC++のエキスパートが作ってくれたスマートポインタを流用できるならそれが一番いいわけです。
例えばwin32でファイルを管理するのはHANDLEという特殊なポインタの様な型になりますが、これ専用のスマートポインタを作るとするとコピーしたときの振る舞いとか、参照カウンタがどうとか実装するのが面倒です。しかも、同じHANDLEでもミューテクスオブジェクトとかに使うときは、無効な値が違うのでデストラクタを分けないといけません。似たようなスマートポインタを沢山作るのもアホらしいです。生ポインタを取りだす部分とか殆どのメソッドの実装は全スマートポインタで殆ど共通です。しかしデストラクタが違うのでテンプレートにするわけにもいきません。
そこで、デストラクタの部分、つまりはデリータを指定できるstd::shared_ptrは素晴らしかった訳です。
std::shared_ptrではカスタムデリータはコンストラクタの第2引数で指定します。関数オブジェクトを取るので、関数ポインタ、関数オブジェクト、ラムダ式となんでも取れるのが強みです。

#include <memory>
#include <windows.h>

// 関数カスタムデリータ
void closeFile(HANDLE handle) {
    if (handle != INVALID_HANDLE_VALUE) {
        CloseHandle(handle);
    }
}

// 関数オブジェクトカスタムデリータ
struct FileDeleter {
    void operator ()(HANDLE handle) {
        if (handle != INVALID_HANDLE_VALUE) {
            CloseHandle(handle);
        }
    }
};

int main() {
    // ファイルハンドルを管理するstd::shared_ptr
    // 関数で指定
    std::shared_ptr<void> file1(ファイルハンドル, closeFile);
    // 関数オブジェクトで指定
    std::shared_ptr<void> file2(ファイルハンドル, FileDeleter());
    // ラムダ式で指定
    std::shared_ptr<void> file3(ファイルハンドル, [](HANDLE handle) {if (handle != INVALID_HANDLE_VALUE) {CloseHandle(handle);}});
}

このなんでも取れるという柔軟性は便利だと思います。しかし同じHANDLEでもデリータを変える必要があるミューテクス等も扱うときには混乱しそうです。入れるものに対して正しいカスタムデリータを入れることを生成時に注意する必要があるためです。
また、shared_ptrのテンプレート引数にHANDLEのポインタの種類が何かを指定しなければなりません。HANDLEは今のところvoid *であるのでvoidを指定してやればいいのですが、元々HANDLE等とtypedefしているのはHANDLEが何のポインタかを意識させないためにやっていると言っても過言ではないのでこれでは本末転倒です。今後の更新でHANDLEが何か別のポインタになるかもしれませんし。typedefとはそういう時にコードの変更をしなくて済むようにするためのものだったはずです。
一方std::unique_ptrの方のカスタムデリータは指定の仕方が全く違います。テンプレート第2引数にカスタムデリータの型を指定するのでstd::shared_ptrの時と違って関数オブジェクトしか指定できません。

#include <memory>
#include <windows.h>

// 関数オブジェクトカスタムデリータしか駄目
struct FileDeleter {
    typedef HANDLE pointer;
    void operator ()(HANDLE handle) {
        if (handle != INVALID_HANDLE_VALUE) {
            CloseHandle(handle);
        }
    }
};

int main() {
    // ファイルハンドルを管理するstd::unique_ptr
    std::shared_ptr<HANDLE, FileDeleter> file(ファイルハンドル);
}

カスタムデリータでtypedef HANDLE pointer;としてあげることで、テンプレート第一引数にはstd::shared_ptrの時と違ってHANDLEをそのまま入れることが出来ます。これでstd:shared_ptrの時とは違い、typedefの中身を実装が変えてしまったときにも変更の必要がなくなりました。またテンプレート第二引数で、カスタムデリータを指定することによって、誤ったカスタムデリータを指定する可能性が減ると思います。例えばミューテクスとファイルを同時に管理するとしても、

// ファイル用カスタムデリータ
struct FileDeleter {
    typedef HANDLE pointer;
    void operator ()(HANDLE handle) {
        if (handle != INVALID_HANDLE_VALUE) {
            CloseHandle(handle);
        }
    }
};

// ミューテクス用カスタムデリータ
struct MutexDeleter {
    typedef HANDLE pointer;
    void operator ()(HANDLE handle) {
        CloseHandle(handle);
    }
};

typedef std::unique_ptr<HANDLE, FileDeleter> FilePtr;
typedef std::unique_ptr<HANDLE, MutexDeleter> MutexPtr;

としてやれば、流石に間違えることはないでしょう。
さてさて、どういうわけか同じ標準ライブラリであるにも関わらず、何故かカスタムデリータの指定方法が違うstd::unique_ptrとstd::shared_ptr。どちらの方式にも魅力はありますが、個人的にはどちらかに統一して欲しいものです。
追記:
std::unique_ptrで関数をカスタムデリータに使用出来ることが判明しました。

// 関数カスタムデリータ
void closeFile(HANDLE handle) {
    if (handle != INVALID_HANDLE_VALUE) {
        CloseHandle(handle);
    }
}

int main() {
    std::unique_ptr<std::remove_pointer<HANDLE>::type, decltype(&closeFile)> file(ファイルハンドル, closeFile);
    return 0;
}

std::remove_pointer::typeってとこがミソです。これによりHANDLEをポインタと見なさせることができます。また、decltypeは何かを入れるとのそ型を返してくれます。これで関数ポインタの型を取ってきてデリータの型として指定します。関数名の前に&が必要です。そして第2引数にデリータを入れてやります。
また、std::remove_pointer::typeを使ってやれば、std::shared_ptrでもHANDLEのtypedefを気にせずに入れることができました。

int main() {
    // ファイルハンドルを管理するstd::shared_ptr
    // 関数で指定
    std::shared_ptr<std::remove_pointer<HANDLE>::type> file1(ファイルハンドル, closeFile);
    // 関数オブジェクトで指定
    std::shared_ptr<std::remove_pointer<HANDLE>::type> file2(ファイルハンドル, FileDeleter());
    // ラムダ式で指定
    std::shared_ptr<std::remove_pointer<HANDLE>::type> file3(ファイルハンドル, [](HANDLE handle) {if (handle != INVALID_HANDLE_VALUE) {CloseHandle(handle);}});
}

追記:Nullableということ
ファイル用のカスタムデリータは恐らく事故の元になるのではないかという気がしてきました。というのも、CreateFile函数が失敗したときに返すのはINVALID_HANDLE_VALUEというのが大体-1であり、0ではありません。ということは0はもしかしたら有効なファイルハンドルなのではないかということになります。となると0の場合もきちんとCloseHandleしてやらないとまずいのではないかというわけです。とすると、所謂スマートポインタは0の時はデリータを呼び出さないということになっているのでリソースリークの元になるかもしれません。と、このように、0以外が所謂ポインタで言う所のNULLに当たるもの達はその値をNULLと扱うような適切なラッパに包んでやる必要がるかもしれません。因みにもしCreateFileが0は有効なハンドルとして返さないと決まっているのであればこのままでもいいのかもしれませんがその辺りはよくわかりませんので念のためこういったことも考慮しといた方がよさそうです。