为什么unique_ptr需要两个模板参数,而shared_ptr只需要一个?

79

unique_ptrshared_ptr 都可以接受自定义的删除器来调用它们所拥有的对象。但是在 unique_ptr 的情况下,删除器作为类的模板参数传递,而 shared_ptr 的自定义删除器类型则需要在构造函数的模板参数中指定。

template <class T, class D = default_delete<T>> 
class unique_ptr
{
    unique_ptr(T*, D&); //simplified
    ...
};

并且

template<class T>
class shared_ptr
{
    template<typename D>
    shared_ptr(T*, D); //simplified
    ...
};

我不明白为什么会有这样的差异。是什么导致了这种情况?


12
shared_ptr类型擦除了删除器,即shared_ptr的用户不必知道删除器的类型。这会产生运行时开销(分配、解引用),因此unique_ptr不执行此操作(没有额外开销)。例如,请参见https://dev59.com/-Ww15IYBdhLWcg3w6f80。 - dyp
2
@dyp 好的,但是为什么shared_ptr会这样做呢? - qdii
4
@qdii shared_ptr 表示共享所有权,不需要所有所有者都知道如何销毁对象,这可能已经足够提供此类型的擦除。由于簿记对象的存在,开销也不会更大。 - dyp
1
@dyp 我有点怀疑,因为C++的口号是“按需付费”:类型擦除析构函数是有代价的,所以必须有充分的理由才需要它。你说的“不需要所有所有者都知道如何销毁对象”,这不就是共享所有权的定义吗?任何一个所有者都可能需要销毁对象。 - qdii
4
如果我没记错的话,有一些 shared_ptr 的实现方式几乎没有类型抹除的额外开销,因为它们只是将其与所需用于记录管理数据的开销结合在一起。--虽然任何所有者可能需要销毁 shared_ptr 拥有的东西,但所有者不需要知道如何做到这一点,也就是说,它不需要查看所拥有的东西的释放函数的定义或声明。 - dyp
显示剩余3条评论
2个回答

66
如果你将deleter作为模板参数提供(例如unique_ptr),它就是类型的一部分,不需要在该类型的对象中存储任何额外信息。
如果deleter作为构造函数的参数传递(例如shared_ptr),则需要在对象中存储它。这是增加灵活性的代价,因为可以为相同类型的对象使用不同的删除器。
我猜这就是原因:unique_ptr应该是非常轻量级的对象,没有任何开销。对每个unique_ptr都存储deleter可能会使其大小增加一倍。因此人们会使用老式的裸指针,但这样做是不正确的。
另一方面,shared_ptr并不那么轻量级,因为它需要存储引用计数,因此存储自定义deleter看起来是一个不错的权衡。

1
你可以为同一类型的对象使用不同的删除器:您的意思是标准想允许两个指向相同对象的 shared_ptr 以不同的方式被销毁吗? - qdii
3
@qdii - 不是同一个对象,而是相同类型的不同shared_ptr。 - Wojtek Surowka
1
@WojtekSurowka 等等,如果它们指向同一类型 T 的不同对象,将析构函数作为类参数传递就足以让用户为每个对象调用不同的析构函数。只需使用 std::shared_ptr<T, D1> 用于第一个对象,std::shared_ptr<T, D2> 用于第二个对象即可。 - qdii
2
@qdii: 你需要想象一个接受shared_ptr作为参数的函数,或者一个将shared_ptr存储为成员变量的对象。不必关心删除器使它们更加灵活。 - Nemo
2
这都是权衡取舍的问题。unique_ptr 不应该有任何开销,因此你描述的复杂性是必要的代价。对于 shared_ptr,一些开销已经存在,因此它可以以更灵活的方式设计。 - Wojtek Surowka
显示剩余7条评论

3

不同类型的共享指针可以共享同一对象的所有权。请参见std::shared_ptr::shared_ptr重载(8)。唯一指针不需要这样的机制,因为它们不共享

template< class Y > 
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;

如果你不使用类型擦除删除器,你就无法将 shared_ptr<T, Y_Deleter> 用作 shared_ptr<T>,这将使其基本上没有用处。
为什么需要这样的重载?
考虑一下。
struct Member {};
struct Container { Member member };

如果您想在使用Member时保持Container的活动状态,可以执行以下操作:

std::shared_ptr<Container> pContainer = /* something */
std::shared_ptr<Member> pMember(pContainer, &pContainer->member);

你只需要持有pMember(可以将其放入std::vector<std::shared_ptr<Member>>)即可。

或者,您也可以使用重载(9)。

template< class Y > 
shared_ptr( const shared_ptr<Y>& r ) noexcept; 
  // Only exists if Y* is implicitly convertible to T*

您可以进行多态共享。

struct Base {};
struct Derived : Base {};

void operate_on_base(std::shared_ptr<Base>);

std::shared_ptr<Derived> pDerived = /* something*/
operate_on_base(pDerived);

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