std::unique_ptr和std::shared_ptr的销毁行为差异的原理是什么?

7

来自 http://en.cppreference.com/w/cpp/memory/unique_ptr:

如果 T 是某个基类 B 的派生类,则 std::unique_ptr<T> 隐式转换为 std::unique_ptr<B>。结果的 std::unique_ptr<B> 的默认删除器将使用 B 的 operator delete,这会导致未定义行为,除非 B 的析构函数是虚拟的。请注意,std::shared_ptr 的行为不同: std::shared_ptr<B> 将使用类型 T 的 operator delete,即使 B 的析构函数不是虚拟的,它也能正确删除所拥有的对象。

上述描述中,在销毁时行为差异的原理是什么?我最初的猜测是性能方面的问题?

另外有趣的是如何在 B 的析构函数不是虚拟的情况下,std::shared_ptr<B> 能够调用类型 T 的析构函数?


1
这可能有所帮助:https://dev59.com/p2w15IYBdhLWcg3wCXKJ - Eldad Mor
1个回答

8

std::shared_ptr<X>相对于原始的B*有一些额外的开销。

shared_ptr<X>基本上维护4个东西。它维护一个指向B的指针,它维护两个引用计数(一个是“硬”引用计数,另一个是weak_ptr的“软”引用计数),以及它维护一个清理函数。

清理函数是shared_ptr<X>行为不同的原因。当您创建一个shared_ptr<X>时,会创建一个调用该特定类型析构函数的函数,并将其存储在由shared_ptr<X>管理的清理函数中。

当您更改所管理的类型(B*变为C*)时,清理函数保持不变。

因为shared_ptr<X>需要管理引用计数,所以清理函数存储的额外开销是微不足道的。

对于unique_ptr<B>,该类几乎与原始的B*一样便宜。它除了B*之外没有任何状态,并且其行为(在销毁时)归结为if (b) delete b;。(是的,那个if (b)是多余的,但优化器可以找出来)。

为了支持向基类转换和作为派生类删除,必须存储额外的状态,以记住unique_ptr实际上是一个派生类。这可以采用存储指向删除器的指针的形式,例如shared_ptr

然而,这将使unique_ptr<B>的大小翻倍,或者需要在堆上存储数据。

决定让unique_ptr<B>没有开销,因此它不支持向基类转换并调用基类的析构函数。

现在,您可能可以通过添加删除器类型并存储知道正在销毁的东西类型的销毁函数来教会unique_ptr<B>执行此操作。上述内容讨论了unique_ptr的默认删除器,该删除器是无状态且微不足道的。

struct deleter {
  void* state;
  void(*f)(void*);
  void operator()(void*)const{if (f) f(state);}
  deleter(deleter const&)=default;
  deleter(deleter&&o):deleter(o) { o.state = nullptr; o.f=nullptr; }
  deleter()=delete;
  template<class T>
  deleter(T*t):
    state(t),
    f([](void*p){delete static_cast<T*>(p);})
  {}
};
template<class T>
using smart_unique_ptr = std::unique_ptr<T, deleter>;

template<class T, class...Args>
smart_unique_ptr<T> make_smart_unique( Args&&... args ) {
  T* t = new T(std::forward<Args>(args)...);
  return { t, t };
}
这是一个实时示例,我会生成一个unique-ptr指向派生类,将其存储在unique-ptr指向基类中,然后重置基类。此时,派生指针被删除。
需要注意的是,在不更改deleter的情况下更改存储在此类unique-ptr中的指针将导致不良行为。另外,一个简单的void(*)(void*) deleter可能会遇到问题,因为传递的void*在基类和派生类之间的值可能会有所不同。

好的答案,但是在一般情况下unique_ptr并不真正具有零开销。它也必须存储一个deleter。不像shared_ptr,尽管通过模板参数,deleter类型是其类型的一部分。对于零大小的deleter,可以通过空基类优化实现真正的零开销。 - Angew is no longer proud of SO
@Angew 我总是忘记它是否进行了if测试。我确实认为我见过一个在那里做错事情的实现。 - Yakk - Adam Nevraumont
@AaronMcDaid 我已经添加了一个更智能的unique ptr,只需使用第二个参数deleter即可存储要删除的内容。它比unique_ptr有更多的开销。 - Yakk - Adam Nevraumont
if(b) 部分对于无法处理 null 值的自定义删除器非常重要。此外,std::function 是一个糟糕的删除器 - 它的构造函数可能会抛出异常。 - T.C.
@T.C. 同意。已编写轻量级双指针“删除器”。不认为我们可以通过无状态方式实现此操作,因为基本上需要第二个指针来确定如何从“base”中删除“derived”。也许可以通过仔细使用子类型删除器来实现,其中“unique_ptr<base,deleter<base>>”记录“void*->base->derived”,然后进行删除?基本上跟踪强制转换到基类如何更改“void*”并展开。棘手的。 - Yakk - Adam Nevraumont
显示剩余2条评论

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