为什么 shared_ptr 删除器必须是 CopyConstructible?

Why do shared_ptr deleters have to be CopyConstructible?

在 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 是 CopyConstructible。为什么需要这个?如果 shared_ptr 复制 d 那么这些删除器中的哪些可能被调用? shared_ptr 不可能只保留一个删除器吗?如果 d 可以被复制,shared_ptr 拥有 删除器意味着什么?

CopyConstructible 要求背后的基本原理是什么?

PS:此要求可能会使 shared_ptr 的删除程序复杂化。 unique_ptr 似乎对其删除器有更好的要求。

因为 shared_ptr 意味着要被复制,而这些复制中的任何一个都可能必须删除该对象,所以它们都必须有权访问删除器。只保留一个删除器需要重新计算删除器本身。如果您真的希望发生这种情况,您可以使用嵌套的 std::shared_ptr 作为删除器,但这听起来有点矫枉过正。

这个问题非常令人困惑,以至于我给 Peter Dimov(boost::shared_ptr 的实施者并参与 std::shared_ptr 的标准化)发了电子邮件

以下是他所说的要点(经他许可转载):

My guess is that the Deleter had to be CopyConstructible really only as a relic of C++03 where move semantics didn’t exist.

你猜对了。当 shared_ptr 被指定为右值引用时 还不存在。现在我们应该能够满足要求 nothrow move-constructible.

其中有一个微妙之处

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

抛出,d 必须完好无损才能使清理 d(p) 正常工作,但我 认为这不会是一个问题(虽然我实际上并没有 试图使实施移动友好)。
[...]
我认为不会有问题 定义它的实现,以便当 new 抛出时, d 将被保留 处于原始状态。

如果我们更进一步,允许 D 有一个投掷构造函数,事情就会变得 更复杂。但我们不会。 :-)

std::shared_ptrstd::unique_ptr 中删除器的区别在于 shared_ptr 删除器是类型擦除的,而 unique_ptr 中删除器类型是模板的一部分。

Here is Stephan T. Lavavej explaining 类型擦除如何导致 std::function 中的 CopyConstructible 要求。

至于这种指针类型差异背后的原因,已在 SO 上多次解决,例如.

引述什么 S.T.L。说:

Very surprising "gotcha" I would say is that the std::function requires CopyConstructible function objects, and this is kind of unusual in the STL.

Usually the STL is lazy in the sense that it doesn't need things up front: if I have something like a std::list of type T, T does not need to be less-than-comparable; only if you call the member function list<T>::sort then it actually does need to be less-than-comparable.

The core language rule that powers this is that the definitions of member functions of a class template are not instantiated until they're actually needed and the bodies don't exist in some sense until you actually call it.

This is usually cool - this means you only pay for what you need, but std::function is special because of type erasure, because when you construct the std::function from some callable object F it needs to generate everything you could ever need from that object F because it's going to erase its type. It requires all the operations that it could possibly ever need regardless of if they're used.

So if you construct a std::function from some callable object F, F is absolutely required at compile-time to be CopyConstructible. This is true even though F will be moved into the std::function, so even if you give it an r-value and even if you never copy std::functions anywhere in your program, F is still required to be CopyConstructible.

You'll get a compiler error saying so - maybe horrible, maybe nice - depending on what you get.

It just cannot store movable only function objects. This is a design limitation caused in part by the fact that std::function dates back to boost/TR1, before r-value references, and in some sense it can never be fixed with std::function's interface as it stands.

Alternatives are being investigated, maybe we can have a different "movable function", so we will probably get some sort of type-erased wrapper that can store movable only function in the future, but std::function as it stands in c++17 right now cannot do that, so just be aware.