为什么shared_ptr的删除器必须是可复制构造的?

40
在C++11中,std::shared_ptr有四个构造函数可以传递类型为D的删除器对象d。这些构造函数的签名如下:
template<class Y, class D> shared_ptr(Y * p, D d);
template<class Y, class D, class A> shared_ptr(Y * p, D d, A a);
template <class D> shared_ptr(nullptr_t p, D d);
template <class D, class A> shared_ptr(nullptr_t p, D d, A a);

标准要求在 [util.smartptr.shared.const] 类型 D 中需要具备可复制构造函数。为什么需要这样做?如果 shared_ptr 复制了 d,那么哪个删除器可能会被调用?难道一个 shared_ptr 只能保留一个删除器吗?如果 d 能够被复制,那么 shared_ptr 拥有删除器是什么意思? CopyConstructible 要求背后的原理是什么? PS:此要求可能会使编写 shared_ptr 的删除器变得更加复杂。相比之下,unique_ptr 对其删除器有更好的要求。

7
通过运动。 - Nicol Bolas
3
概念上是可以的。但是一种类型可以是可移动构造而不是可复制构造,例如 unique_ptr。他的问题实际上是为什么shared_ptr禁止使用仅可移动的删除器类型。 - Nicol Bolas
实现线程安全更容易吗?例如,大多数成员都是复制的,因此只需要处理少数共享成员? - user3528438
7
我怀疑答案是:它是在没有移动语义的时代设计的。目前我想不到什么与仅移动删除器存在根本性问题。 - T.C.
4
也许是因为只有可拷贝构造的对象才能被 std::function 持有,导致出现这种情况? - user1887915
显示剩余6条评论
3个回答

23

这个问题令人困惑,我给Peter Dimov(boost::shared_ptr 的实现者,也参与了 std::shared_ptr 的标准化)发送了电子邮件。

以下是他回复的要点(经过他的允许再次印刷):

我的猜测是,Deleter 只有在 C++03 中存在没有移动语义的遗留问题时才需要 CopyConstructible。

你的猜测是正确的。当规定 shared_ptr 时,右值引用还不存在。现在我们只需要要求 nothrow move-constructible 就可以了。

有一个细节是

pi_ = new sp_counted_impl_pd<P, D>(p, d);

为了让清理函数d(p)能够正常工作,需要保持d不变,但我认为这不是问题(虽然我实际上没有尝试过使实现具有可移动性)。 [...] 我认为,当new抛出异常时,实现可以定义使d保持原始状态,这将不会有任何问题。

如果我们进一步允许D具有抛出移动构造函数,则情况会变得更加复杂。但我们不会这样做。:-)


4
我一直想知道这个问题的答案,并努力寻找参考资料。感谢你的提问 :) - OmnipotentEntity
这对我来说看起来像是一个标准变更提案的候选人。为什么还没有人提出呢?这个答案已经有6年了。 - Ivan Kolev

3
std::shared_ptrstd::unique_ptr中deleter的区别在于,shared_ptr的deleter是类型擦除的,而unique_ptr的deleter类型是模板的一部分。
Stephan T. Lavavej在这里解释了类型擦除如何导致std::function中需要CopyConstructible要求。
至于指针类型之间差异的原因,在SO上已经多次讨论过,例如这里
以下是S.T.L.的引言:
非常惊人的"坑"是,std::function需要可复制构造的函数对象,这在STL中有点不寻常。通常情况下,STL是懒惰的,因为它不需要事先准备好所有的东西:如果我有一个类型为Tstd::list,那么T不需要是可比较的;只有当你调用成员函数list<T>::sort时,它才需要是可比较的。支持这一特性的核心语言规则是,类模板的成员函数的定义直到实际需要它们,并且在某种意义上直到你实际调用它们时它们才存在。通常来说,这很酷——这意味着你只为你需要的付费,但std::function由于类型抹除而变得特殊,因为当你从某个可调用对象F构造std::function时,它需要从该对象F生成你可能需要的所有内容,因为它将要抹去其类型。它需要所有可能需要的操作,而不管它们是否被使用。所以,如果你从某个可调用对象F构造一个std::function,那么在编译时,F绝对需要是可复制构造的。即使你将一个r值传递给它,即使你在程序中从来没有复制std::functionF仍然要求是可复制构造的。你会收到一条编译器错误提示——可能很糟糕,也可能很好,这取决于你得到了什么。它只能存储可移动的函数对象。这是一种设计限制,部分原因是因为std::function可以追溯到boost/TR1之前,在r值引用之前,某种意义上,它永远无法通过std::function的接口来修复。我们正在调查替代方案,也许我们可以有一个不同的"可移动函数",所以我们或许将来会得到某种类型抹除的包装器,可以存储可移动的函数,但是就目前而言,std::function在c++17中不能做到这一点,所以请注意。

@jotik 我刚刚添加了整个引用,这是我从YouTube的关闭字幕中提取出来的,希望这样可以。 - Ap31

-2

因为 shared_ptr 旨在被复制,而任何这些副本都可能需要删除对象,因此它们都必须具有对删除器的访问权限。仅保留一个删除器将需要对删除器本身进行引用计数。如果您真的希望发生这种情况,可以使用嵌套的 std::shared_ptr 作为删除器,但这听起来有点过度设计。


9
但是删除器已经有引用计数了。大多数实现将其与要删除的指针一起放在控制块中。复制一个 shared_ptr 不会复制删除器。 - Nicol Bolas
这是否是特定的(即在标准中指定)?如果是这种情况,您仍然需要将其复制到refcounted块中一次,但我已经纠正了。 - mefyl
2
不确定为什么你被踩了。一个(尽管是天真的)实现这样做完全是合理的。 - gmbeard
不要误会,他们说得很有道理:它可以被设计成只需要一个副本,并且可以进行移动。因此,虽然我的答案可能是为什么必须可复制的潜在解释,但它是相当不准确的。 - mefyl
@NicolBolas,标准仍然不要求删除器是引用计数的。这就解释了为什么删除器必须是可复制构造的,这是完全有道理的。 - Andrei R.
@AndreiR.:“标准不要求删除器是引用计数的。” 是的,确实如此。但是,在不将删除器与控制块一起存储的情况下实现shared_ptr的唯一方法是在每个shared_ptr中使用类型擦除值类型存储它。对于weak_ptr也是如此。为什么你会以那种方式实现它呢?主要问题是删除器不需要是可复制构造的,只是为了支持愚蠢的shared_ptr实现。 - Nicol Bolas

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