为什么 shared_ptr<void> 合法,而 unique_ptr<void> 合法 ill-formed?

Why is shared_ptr<void> legal, while unique_ptr<void> is ill-formed?

这个问题确实符合标题:我很想知道造成这种差异的技术原因是什么,还有基本原理?

std::shared_ptr<void> sharedToVoid; // legal;
std::unique_ptr<void> uniqueToVoid; // ill-formed;

因为std::shared_ptr实现了类型擦除,而std::unique_ptr没有。


由于std::shared_ptr实现了类型擦除,它还支持另一个有趣的属性,即。它不需要删除器的类型作为class模板的模板类型参数。看看他们的声明:

template<class T,class Deleter = std::default_delete<T> > 
class unique_ptr;

具有 Deleter 作为类型参数,而

template<class T> 
class shared_ptr;

没有。

那么,为什么 shared_ptr 实现类型擦除?

嗯,它这样做了,因为它必须支持引用计数,并且为了支持它,它必须从堆中分配内存,因为它 必须 分配内存,它更进一步并实现了类型擦除——这也需要堆分配。 所以基本上就是投机取巧!

由于类型擦除,std::shared_ptr 能够支持两件事:

  • 它可以将任何类型的对象存储为 void*但它仍然能够通过正确地调用它们来正确地删除销毁对象析构函数.
  • 删除器的类型不会作为类型参数传递给 class 模板,这意味着在不影响类型安全的情况下 有一点自由 .

好的。这就是 std::shared_ptr 的工作原理。

现在的问题是,std::unique_ptr可以将对象存储为void*吗?好吧,答案是 yes — 前提是您传递一个合适的删除器作为参数。这是一个这样的演示:

int main()
{
    auto deleter = [](void const * data ) {
        int const * p = static_cast<int const*>(data);
        std::cout << *p << " located at " << p <<  " is being deleted";
        delete p;
    };
    
    std::unique_ptr<void, decltype(deleter)> p(new int(959), deleter);
    
} //p will be deleted here, both p ;-)

输出(online demo):

959 located at 0x18aec20 is being deleted

你在评论中提出了一个非常有趣的问题:

In my case I will need a type erasing deleter, but it seems possible as well (at the cost of some heap allocation). Basically, does this mean there is actually a niche spot for a 3rd type of smart pointer: an exclusive ownership smart pointer with type erasure.

@Steve Jessop 向其提出了以下解决方案,

I've never actually tried this, but maybe you could achieve that by using an appropriate std::function as the deleter type with unique_ptr? Supposing that actually works then you're done, exclusive ownership and a type-erased deleter.

按照这个建议,我实现了这个(虽然它没有使用 std::function,因为它似乎没有必要):

using unique_void_ptr = std::unique_ptr<void, void(*)(void const*)>;

template<typename T>
auto unique_void(T * ptr) -> unique_void_ptr
{
    return unique_void_ptr(ptr, [](void const * data) {
         T const * p = static_cast<T const*>(data);
         std::cout << "{" << *p << "} located at [" << p <<  "] is being deleted.\n";
         delete p;
    });
}

int main()
{
    auto p1 = unique_void(new int(959));
    auto p2 = unique_void(new double(595.5));
    auto p3 = unique_void(new std::string("Hello World"));
}  

输出(online demo):

{Hello World} located at [0x2364c60] is being deleted.
{595.5} located at [0x2364c40] is being deleted.
{959} located at [0x2364c20] is being deleted.

希望对您有所帮助。

理由之一是 shared_ptr 的众多用例之一 - 即作为生命周期指示器或哨兵。

原始的 boost 文档中提到了这一点:

auto register_callback(std::function<void()> closure, std::shared_ptr<void> pv)
{
    auto closure_target = { closure, std::weak_ptr<void>(pv) };
    ...
    // store the target somewhere, and later....
}

void call_closure(closure_target target)
{
    // test whether target of the closure still exists
    auto lock = target.sentinel.lock();
    if (lock) {
        // if so, call the closure
        target.closure();
    }
}

其中 closure_target 是这样的:

struct closure_target {
    std::function<void()> closure;
    std::weak_ptr<void> sentinel;
};

调用者会像这样注册一个回调:

struct active_object : std::enable_shared_from_this<active_object>
{
    void start() {
      event_emitter_.register_callback([this] { this->on_callback(); }, 
                                       shared_from_this());
    }

    void on_callback()
    {
        // this is only ever called if we still exist 
    }
};

因为 shared_ptr<X> 总是可以转换为 shared_ptr<void>,event_emitter 现在可以幸福地不知道它回调到的对象类型。

这种安排释放了事件发射器的订阅者处理交叉情况的义务(如果回调在队列中,等待在 active_object 消失时被采取行动怎么办?),也意味着有不需要同步退订。 weak_ptr<void>::lock 是一个同步操作。