使用抽象删除器与std::unique_ptr

5
我希望拥有一个运行时接口,提供一些创建方法。这些方法返回 unique_ptr<T>,我想让创建类启用自定义删除。问题是,我绝对不想让接口直接提供这些方法-它们只应该在 unique_ptr<T, SomeCustomDel> 的销毁中可用。现在,我想到可以使用std::unique_ptr<T, std::function<void(T*)>>,但我真的不想这样做,因为我根本不需要那种抽象层次,也不想支付堆分配的代价。
有什么建议吗?

unique_ptr 的自定义删除器版本到底哪里出了问题?你确定它比标准删除器版本产生更多开销吗?MSVC 中 shared_ptr 的实现是 Advanced STL episode 中第一个讨论的话题,其中两个版本的 shared_ptr 类在内部复杂性方面非常相似。如果返回 unique_ptr<T> 是最清晰的解决方案,也许您不需要太过担心它的代价。 - Kerrek SB
@Kerrek SB:重点不在于unique_ptr的开销,而在于std::function的额外开销。 - Puppy
5个回答

2

我希望有一个标准的"动态"删除器版本的std::unique_ptr。这个神秘的类会允许我在实例化时附加一个删除器到unique_ptr,就像std::shared_ptr一样。

话虽如此,如果这样的类型存在,我认为它基本上是用std::unique_ptr<T,std::function<void(T*)>>实现的。这正是你想要避免的事情。

然而,我认为你低估了std::function。它的实现是优化,以避免尽可能地击中堆栈。如果您的删除器对象保持较小,则所有操作都将在堆栈上完成(我认为boost::function可以静态处理大小不超过32字节的删除器)。

对于一个太普遍的删除器问题。您必须提供删除器的定义。没有其他方法。但是,您不必让用户实例化该类,这基本上禁止他们使用它。为此,请使删除器的构造函数需要一个标记结构,在实现文件中仅定义该结构。

或者可能最简单的解决方案。将删除器放入详细信息命名空间中。用户仍然可以自由使用它,但很明显他们不应该并且不能抱怨您更改它时破坏了他们的代码。


2
您的规格说明对我来说不是很清楚,但您是否考虑过使用unique_ptr<T, void(*)(void*)>?这是一种非常灵活的类型,具有许多动态删除器的特性。
如果这不是您要寻找的内容,您可以尝试以下类似的内容:
class impl
{
public:
    virtual ~impl();

    virtual void operator()(void*) = 0;
    virtual void other_functionality() = 0;
};

class my_deleter
{
    impl* p_;
public:
    ...
    void operator()(void* p) {(*p_)(p);}
    void other_functionality() {p_->other_functionality();}
    ...
};

如果没有更多关于您需求的详细信息,很难确定什么最适合您。


我最终选择了这个,以模板形式。 - Puppy
我无法看出第二部分相比于 std::function<void(void*)> 作为删除器有任何改进。 - sellibitze
因为一个好的模板可以清理掉那些可怕的类型不安全的 void*,而且 impl 可以直接指向分配类。由于 unique_ptr 是在另一侧创建的,所以我不需要在主要分配类中公开这个接口,这正是我想要的。 - Puppy

1

我看到两个选项。

选项1: 使用包含函数指针和必要时可编码一些状态的原始 char 数组的自定义删除器:

template<class T>
void simply_delete(T* ptr, const unsigned char*) {
    delete ptr;
}

template<class T, int StateSize>
struct my_deleter {
    void (*funptr)(T*,const unsigned char*);
    array<unsigned char,StateSize> state;

    my_deleter() : funptr(&simply_delete<T>) {}

    void operator()(T* ptr) const {
        funptr(ptr,StateSize>0 ? &state[0] : nullptr);
    }
};

template<class T>
using upi = unique_ptr<T,my_deleter<T,sizeof(void*)>>;

现在,您可以创建不同的upi<T>对象,存储不同的函数指针和删除器状态,而无需在其类型中提及确切发生了什么。但这几乎与实现“小型函数优化”的function<>删除器相同。您可以期望一个体面的标准库实现为小型函数对象(如函数指针)提供非常高效的function<>包装器,而不需要任何堆分配。至少我是这样认为的。:)

选项2:只需使用shared_ptr而不是unique_ptr,并利用其内置的类型抹除功能来处理删除器。这也将使您能够轻松支持Derived->Base转换。为了最大程度地控制分配的位置,您可以使用std::allocate_shared函数模板。


1

这是对其中一个答案的回复,而不是原始问题的回复。由于格式原因,它是一个答案而不是评论。

我希望有一个标准的“动态”删除器版本的std::unique_ptr。这个神话般的类将允许我在实例化时附加一个删除器到unique_ptr,类似于std::shared_ptr

这里是这样一个类的实现的开始。这相当容易做到。我只使用unique_ptr作为异常安全辅助,没有更多的用途。它不像你想象的那样功能齐全。这些额外的功能留给读者自己练习。 :-) 下面的内容建立了指针的唯一所有权和自定义动态删除器的存储。请注意,即使智能指针的构造函数抛出异常(这实际上是unique_ptr在实现中最有用的地方),智能指针仍然拥有传入的指针。

#include <memory>
#include <type_traits>

namespace detail
{

class impl
{
public:
    virtual ~impl() {};
};

template <class T, class D>
class erase_type
    : public impl
{
    T* t_;
    D d_;

public:
    explicit erase_type(T* t)
            noexcept(std::is_nothrow_default_constructible<D>::value)
        : t_(t)
    {}

    erase_type(T* t, const D& d)
            noexcept(std::is_nothrow_copy_constructible<D>::value)
        : t_(t),
          d_(d)
       {}

    erase_type(T* t, D&& d)
            noexcept(std::is_nothrow_move_constructible<D>::value)
        : t_(t),
          d_(std::move(d))
       {}

    virtual ~erase_type()
    {
        if (t_)
            d_(t_);
    }

    erase_type(const erase_type&) = delete;
    erase_type& operator=(const erase_type&) = delete;
};

}  // detail

template <class T>
class my_pointer
{
    T* ptr_;
    detail::impl* impl_;

public:
    my_pointer() noexcept
        : ptr_(nullptr),
          impl_(nullptr)
    {}

    template <class Y>
    explicit my_pointer(Y* p)
        : ptr_(static_cast<T*>(p)),
          impl_(nullptr)
    {
        std::unique_ptr<Y> hold(p);
        impl_ = new detail::erase_type<Y, std::default_delete<Y>>(p);
        hold.release();
    }

    template <class Y, class D>
    explicit my_pointer(Y* p, D&& d)
        : ptr_(static_cast<T*>(p)),
          impl_(nullptr)
    {
        std::unique_ptr<Y, D&> hold(p, d);
        typedef
            detail::erase_type<Y, typename std::remove_reference<D>::type>
            ErasedType;
        impl_ = new ErasedType(p, std::forward<D>(d));
        hold.release();
    }

    ~my_pointer()
    {
        delete impl_;
    }

    my_pointer(my_pointer&& p) noexcept
        : ptr_(p.ptr_),
          impl_(p.impl_)
    {
        p.ptr_ = nullptr;
        p.impl_ = nullptr;
    }

    my_pointer& operator=(my_pointer&& p) noexcept
    {
        delete impl_;
        ptr_ = p.ptr_;
        impl_ = p.impl_;
        p.ptr_ = nullptr;
        p.impl_ = nullptr;
        return *this;
    }

    typename std::add_lvalue_reference<T>::type
    operator*() const noexcept
        {return *ptr_;}

    T* operator->() const noexcept
        {return ptr_;}
};

请注意,与unique_ptr(并像shared_ptr一样)不同,接受指针的构造函数不是noexcept。虽然可以通过使用“小删除器”优化来缓解这种情况。这是留给读者的另一个练习。 :-)

0

我在谷歌上搜索自己的问题时发现了这个问题;使用unique_ptr与抽象基类指针。所有答案都很好。我发现@deft_code的回答最适合我的需求。

这是我在我的情况下最终采取的做法:

假设T是一个抽象基类类型。makeT()函数创建某个派生类的新实例,并返回T*:

std::unique_ptr<T, std::function<void(T*)>> p(makeT(), [](T* p){p.delete();});

我只是想为那些正在寻找简短、复制和粘贴式解决方案的人分享这个。

对于未经训练的C++11开发者来说,[](...语法是一个lambda表达式。

正如其他答案中提到的那样,它不是C语言中的“函数指针”,而是一个可调用的C++对象,非常小巧,携带它的开销应该可以忽略不计。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接