实现 shared_ptr 而不需要要求多态类具有虚析构函数,是否有可能?

98

Lidström先生和我争论过 :)

Lidström先生认为一个这样的构造函数shared_ptr<Base> p(new Derived);并不要求Base必须有虚析构函数:

Armen Tsirunyan: "真的吗? shared_ptr会正确清理吗? 请在此情况下演示如何实现该效果"

Daniel Lidström: "shared_ptr使用自己的析构函数来删除Concrete实例。这在C++社区中被称为RAII。我的建议是,尽可能了解RAII。当您在所有情况下都使用RAII时,它将使您的C++编码变得更加容易。"

Armen Tsirunyan: "我知道RAII,我也知道最终shared_ptr析构函数可以在pn达到0时删除存储的px。但是如果px具有指向Base的静态类型指针和指向Derived的动态类型指针,那么除非Base有虚构造函数,否则这将导致未定义行为。如果我说错了,请纠正我。"

Daniel Lidström: "shared_ptr知道静态类型是Concrete。它知道这一点,因为我在构造函数中传递了它!看起来有点像魔术,但我可以向您保证这是经过设计的,非常好用。"

那么,请评判我们。如何(如果可能)实现shared_ptr而不需要多态类具有虚构造函数?


9
另一个有趣的事情是,shared_ptr<void> p(new Derived) 也会通过它的析构函数销毁 Derived 对象,无论它是否为虚拟的。 - dalle
7
很棒的提问方式 :) - rubenvb
5
尽管shared_ptr允许这样做,但设计一个没有虚析构函数的基类是一个非常糟糕的想法。Daniel关于RAII的评论是误导性的 - 它与此无关 - 但引用的对话听起来像是简单的沟通不畅(以及对shared_ptr如何工作的错误假设)。 - Roger Pate
6
不是 RAII,而是通过类型擦除来处理析构函数。需要注意的是,shared_ptr<T>((T*)new U()) 的情况下,其中 struct U:T 将不会按预期工作(这可以通过间接方式轻松完成,例如一个接受 T* 但传递了 U* 的函数)。 - Yakk - Adam Nevraumont
4
哇,这个问题是在10/10/10几乎10点的时候提出的。 - David G
显示剩余4条评论
3个回答

80
是的,可以这样实现shared_ptr。Boost和C++11标准都要求这种行为。除了引用计数外,shared_ptr还管理更多内容。所谓的“deleter”通常放在同一内存块中,该内存块也包含引用计数器。但有趣的是,此deleter的类型不是shared_ptr类型的一部分。“类型擦除”(type erasure)就是用于实现隐藏实际函数对象类型的“多态函数”boost::function或std::function的基本技术。为使您的示例正常工作,我们需要一个模板化构造函数:
template<class T>
class shared_ptr
{
public:
   ...
   template<class Y>
   explicit shared_ptr(Y* p);
   ...
};

所以,如果你将这个应用到你的类BaseDerived 中...
class Base {};
class Derived : public Base {};

int main() {
   shared_ptr<Base> sp (new Derived);
}

使用模板构造函数 Y = Derived 来构造 shared_ptr 对象。因此,构造函数有机会创建适当的删除器对象和引用计数器,并将指向该控制块的指针存储为数据成员。如果引用计数器达到零,则将使用先前创建的 Derived 感知删除器来处理对象。

C++11标准对此构造函数(20.7.2.2.1)有以下说明:

要求: p 必须可转换为 T * Y 应为完整类型。 表达式 delete p 必须形式良好,具有明确定义的行为,不应引发异常。

效果:构造拥有指针 p shared_ptr 对象

至于析构函数(20.7.2.2.2):

效果:如果 * this 为空或与另一个 shared_ptr 实例共享所有权( use_count()> 1 ),则没有副作用。 否则,如果 * this 拥有对象 p 和删除器 d ,则调用 d(p)否则,如果 * this 拥有指针 p ,则调用 delete p

(我的加粗字体强调)。


(a) 即将到来的标准也要求这种行为。 (b) 您能提供一下参考文献吗? - kevinarpe
我只是想在@sellibitze的答案中添加一条评论,因为我没有足够的积分来“添加评论”。在我看来,这更多是“Boost does this”而不是“标准要求”。根据我的理解,我认为标准并不要求这样做。谈到@sellibitze的示例shared_ptr<Base> sp (new Derived);Requiresconstructor只要求delete Derived被定义和格式良好。对于destructor的规范,也有一个p,但我不认为它指的是constructor规范中的p - Lujun Weng

31
当创建shared_ptr时,它会在内部存储一个deleter对象。当shared_ptr即将释放所指向的资源时,该对象被调用。由于在构造时已知如何销毁资源,因此即使是不完整的类型,也可以使用shared_ptr。创建shared_ptr的人已经在其中存储了正确的deleter。
例如,您可以创建一个自定义的deleter:
void DeleteDerived(Derived* d) { delete d; }
shared_ptr<Base> p(new Derived, DeleteDerived);

p会调用DeleteDerived来销毁指向的对象。

这就是shared_ptr构造函数自动完成的操作,所以在实际使用中,除非你使用其他方式释放内存而不是调用delete,否则你不需要实现这种删除器。


4
对于不完整类型的提醒加一分,当将shared_ptr用作属性时非常方便。 - Matthieu M.

17

简单地说,

shared_ptr使用特殊删除器函数,该函数由构造函数创建,始终使用给定对象的析构函数而不是Base的析构函数。这涉及到一些模板元编程的工作,但它确实有效。

就是这样

template<typename SomeType>
shared_ptr(SomeType *p)
{
   this->destroyer = destroyer_function<SomeType>(p);
   ...
}

1
嗯...有趣,我开始相信这个了 :) - Armen Tsirunyan
1
@Armen Tsirunyan 在开始讨论之前,你应该先查看一下 shared_ptr 的设计描述。这个“deleter 的捕获”是 shared_ptr 的一个重要特性之一... - Paul Michalik
7
@paul_71:我同意你的观点。另一方面,我认为这个讨论不仅对我有用,也对其他不知道shared_ptr这个事实的人有用。所以我想无论如何开始这个讨论也没有什么大错。 :) - Armen Tsirunyan
4
@Armen 当然不是。相反,你在指出shared_ptr<T>的这个非常、非常重要的特性方面做得很好,即使是有经验的C++开发人员也经常忽略它。 - Paul Michalik

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