unique_ptr は template の第二引数で Deleter を指定でき、デストラクタでの解放処理をカスタマイズすることができます。
このカスタム Deleter クラスですが次のように書いてもそれなりに動きます。しかしいろいろと問題があります。
// シンプルだが問題のある MyBadDeleter
template<typename T>
struct MyBadDeleter {
void operator()(T* ptr) const {
// カスタムしたい削除処理に変更する
std::cout << "delete" << std::endl;
delete ptr;
}
};
この記事ではカスタム Deleter のクラスを定義するときにハマりどころをまとめました。
結論
次のようにしましょう。
template<typename T = void>
struct MyDeleter
{
constexpr MyDeleter() noexcept = default;
// 別の Deleter オブジェクトから Deleter オブジェクトを構築します。
// このコンストラクタは U* が T* に暗黙に変換可能な場合にのみ、オーバーロード解決に参加します。
template<
typename U,
typename std::enable_if<std::is_convertible<U*, T*>::value, std::nullptr_t>::type = nullptr
>
MyDeleter(const MyDeleter<U>&) noexcept {}
void operator()(T* ptr) const {
// カスタムしたい削除処理に変更する
std::cout << "delete" << std::endl;
delete ptr;
}
};
template<typename T>
struct MyDeleter<T[]>
{
constexpr MyDeleter() noexcept = default;
// 別の Deleter オブジェクトから Deleter オブジェクトを構築します。
// このコンストラクタは U(*)[] が T(*)[] に暗黙に変換可能な場合にのみ、オーバーロード解決に参加します。
template<
typename U,
typename std::enable_if<std::is_convertible<U(*)[], T(*)[]>::value, std::nullptr_t>::type = nullptr
>
MyDeleter(const MyDeleter<U[]>&) noexcept {}
void operator()(T* ptr) const {
// カスタムしたい削除処理に変更する
std::cout << "delete[]" << std::endl;
delete[] ptr;
}
};
ハマりどころ
配列型の扱い
unique_ptr は配列型も使うことができます。よって配列型の特殊化を行っておかないと、意図しない解放になる可能性があります。
{
std::unique_ptr<int[]> p{new int[3]};
} // 問題なし。 default_delete は配列型では delete[] する
{
std::unique_ptr<int[], MyBadDeleter<int[]>> p{new int[3]};
} // エラー: delete を呼んでしまう
{
std::unique_ptr<int[], MyDeleter<int[]>> p{new int[3]};
} // 問題なし。 配列型で特殊化されているので delete[] する
基底クラスの unique_ptr への代入
派生クラスの unique_ptr を基底クラスの unique_ptr に代入することもできます。(ただし基底クラスのデストラクタが virtual でない場合は未定義動作)
このときカスタム Deleter クラスが派生クラスから基底クラスへ変換できるコピーコンストラクタを持っていないと、代入ができません。
struct B {
virtual ~B() = default;
};
struct D : B {
D() { std::cout << "D::D" << std::endl; }
~D() { std::cout << "D::~D" << std::endl; }
};
{
std::unique_ptr<D> pd{new D};
std::unique_ptr<B> pb = std::move(pd);
} // 問題なし。代入可能。
{
std::unique_ptr<D, MyBadDeleter<D>> pd{new D};
std::unique_ptr<B, MyBadDeleter<B>> pb = std::move(pd);
} // コンパイルエラー: MyBadDeleter<D> から MyBadDeleter<B> が作れない
{
std::unique_ptr<D, MyDeleter<D>> pd{new D};
std::unique_ptr<B, MyDeleter<B>> pb = std::move(pd);
} // 問題なし。代入可能。
標準の default_delete も MyDeleter も、型 D が型 B に暗黙に変換可能な場合にのみコピーコンストラクタで変換が可能になっています。この技法についてはこちらにまとまってます。
shared_ptr は?
ここで作った MyDeleter はそのまま shared_ptr に渡すこともできます。
が、そもそも shared_ptr は unique_ptr と配列型や基底クラスと派生クラス間の代入などの挙動が異なるのでその点は注意しましょう。
参考
cppreference.com: unique_ptr
https://ja.cppreference.com/w/cpp/memory/unique_ptr
cppreference.com: default_delete
https://ja.cppreference.com/w/cpp/memory/default_delete
【C++ Advent Calendar 2016 22日目】C++ で enable_if を使うコードのベストプラクティス
http://secret-garden.hatenablog.com/entry/2016/12/22/032008