【发布时间】:2020-03-14 21:51:02
【问题描述】:
假设我有一个带有自定义分配器和自定义删除器的shared_ptr。
我在标准中找不到任何关于删除器应该存储在哪里的内容:它没有说自定义分配器将用于删除器的内存,也没有说它不会。
这是未指定还是我只是错过了什么?
【问题讨论】:
标签: c++ language-lawyer c++17 shared-ptr allocator
假设我有一个带有自定义分配器和自定义删除器的shared_ptr。
我在标准中找不到任何关于删除器应该存储在哪里的内容:它没有说自定义分配器将用于删除器的内存,也没有说它不会。
这是未指定还是我只是错过了什么?
【问题讨论】:
标签: c++ language-lawyer c++17 shared-ptr allocator
C++ 11 中的 util.smartptr.shared.const/9:
效果:构造一个 shared_ptr 对象,该对象拥有对象 p 和删除器 d。第二个和第四个构造函数应该使用一个 a 的副本来分配内存供内部使用。
第二个和第四个构造函数有这些原型:
template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template<class D, class A> shared_ptr(nullptr_t p, D d, A a);
在最新的草案中,util.smartptr.shared.const/10 与我们的目的等效:
效果:构造一个 shared_ptr 对象,该对象拥有对象 p 和删除器 d。当 T 不是数组类型时,第一个和第二个构造函数启用 shared_from_this 和 p。第二个和第四个构造函数应使用 a 的副本来分配内存供内部使用。如果抛出异常,则调用 d(p)。
因此,如果需要在分配的内存中分配它,则使用分配器。根据现行标准和相关缺陷报告,分配不是强制性的,而是由委员会承担。
虽然shared_ptr的接口允许实现永远没有控制块并且所有shared_ptr和weak_ptr都放在一个链表中,但实际上并没有这样的实现。此外,假设use_count 是共享的,措辞已经过修改。
删除器只需要移动可构造的。因此,shared_ptr 中不可能有多个副本。
可以想象这样一种实现,它将删除器放在专门设计的shared_ptr 中,并在删除特殊的shared_ptr 时移动它。虽然实现看起来是一致的,但它也很奇怪,特别是因为使用计数可能需要一个控制块(用使用计数做同样的事情也许是可能的,但更奇怪)。
我发现的相关 DR:545、575、2434(它们承认所有实现都使用控制块,并且似乎暗示多线程约束在某种程度上要求它)、2802(这需要删除器只能移动可构造的,因此会阻止在多个shared_ptr 之间复制删除器的实现。
【讨论】:
a 的副本)来释放该内存的任何信息。这意味着对a 的副本进行了一些存储。 [util.smartptr.shared.dest] 中没有关于它的信息。
来自std::shared_ptr 我们有:
控制块是一个动态分配的对象,它包含:
从std::allocate_shared 我们得到:
template< class T, class Alloc, class... Args >
shared_ptr<T> allocate_shared( const Alloc& alloc, Args&&... args );
构造一个 T 类型的对象并将其包装在 std::shared_ptr [...] 中,以便为共享指针和 T 对象的控制块使用一个分配。
所以看起来std::allocate_shared 应该用你的Alloc 分配deleter。
编辑:来自n4810 §20.11.3.6 Creation [util.smartptr.shared.create]
1 适用于所有
make_shared、allocate_shared、make_shared_default_init、 和allocate_shared_default_init重载,除非另有说明,否则如下所述。[...]
7 备注: (7.1) — 实现应该执行不超过一次的内存分配。 [注:这提供 效率相当于一个侵入式智能指针。 ——尾注]
[强调我的全部]
所以标准是说std::allocate_shared应该使用Alloc作为控制块。
【讨论】:
make_shared,而不是构造函数本身。不过,我可以将成员用于小型删除器。
T 对象分配一个内存。由于它是为T 对象分配的,所以Alloc 必须用于std::allocate_shared。
我相信这是未指定的。
这里是相关构造函数的说明:[util.smartptr.shared.const]/10
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);效果: 构造一个
shared_ptr对象,该对象拥有对象p和删除器d。当T不是数组类型时,第一个和 第二个构造函数使用p启用shared_from_this。第二 第四个构造函数应使用a的副本来分配内存 供内部使用。如果抛出异常,则调用d(p)。
现在,我的解释是,当实现需要内存供内部使用时,它通过使用a 来实现。这并不意味着实现必须使用此内存来放置所有内容。例如,假设有这个奇怪的实现:
template <typename T>
class shared_ptr : /* ... */ {
// ...
std::aligned_storage<16> _Small_deleter;
// ...
public:
// ...
template <class _D, class _A>
shared_ptr(nullptr_t, _D __d, _A __a) // for example
: _Allocator_base{__a}
{
if constexpr (sizeof(_D) <= 16)
_Construct_at(&_Small_deleter, std::move(__d));
else
// use 'a' to allocate storage for the deleter
}
// ...
};
此实现是否“使用a 的副本来分配内存供内部使用”?是的,它确实。它从不分配内存,除非使用a。这个幼稚的实现有很多问题,但是假设它切换到使用分配器,除了最简单的情况,shared_ptr 是直接从指针构造的,从不复制、移动或以其他方式引用,没有其他并发症。关键是,仅仅因为我们无法想象一个有效的实现本身并不能证明它在理论上不存在。我并不是说这样的实现实际上可以在现实世界中找到,只是标准似乎并没有积极禁止它。
【讨论】:
shared_ptr 对于小类型在堆栈上分配内存。所以不符合标准要求
std::move(__d),并在需要复制时回退到allocate。