shared_ptr的删除器是否存储在自定义分配器分配的内存中?

23

假设我有一个带有自定义分配器和自定义删除器的shared_ptr

我在标准中找不到任何关于删除器应该存储在哪里的说明:它没有说自定义分配器将用于删除器的内存,也没有说它不会使用。

这是未指定的还是我遗漏了什么?

3个回答

12

C++ 11中的util.smartptr.shared.const/9:

效果: 构造一个拥有对象p和删除器d的shared_ptr对象。第二个和第四个构造函数将使用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不是数组类型时,第一和第二个构造函数使p启用shared_­from_­this。第二个和第四个构造函数应使用a的副本来分配内部使用的内存。如果抛出异常,则调用d(p)。
因此,如果需要在分配的内存中分配它,则使用分配器。根据当前标准和相关缺陷报告,委员会假定分配不是强制性的,但不是必须的。
尽管shared_ptr的接口允许实现永远没有控制块并且所有shared_ptr和weak_ptr都放在链接列表中的情况,但实际上不存在这样的实现。此外,措辞已被修改,假定use_count是共享的。
删除器只需可移动构造。因此,在shared_ptr中无法有多个副本。
可以想象一种实现方式,将删除器放在特别设计的shared_ptr中,并在删除特殊的shared_ptr时将其移动。虽然实现似乎符合规范,但也很奇怪,特别是因为使用计数可能需要控制块(也许可以使用相同的方法来处理使用计数,但更奇怪)。
我找到的相关DRs:545, 575, 2434(承认所有实现都使用控制块,并似乎意味着多线程约束在某种程度上需要它),2802(要求删除器只可移动构造,从而防止删除器在几个shared_ptr之间复制的实现)。

2
为内部使用分配内存。如果实现一开始不打算为内部使用分配内存,它可以使用成员。 - L. F.
1
@L.F. 不行,接口不允许这样做。 - AProgrammer
从理论上讲,它仍然可以使用某种“小删除优化”,对吧? - L. F.
@DanielsaysreinstateMonica,我同意那很奇怪。我也找不到它。 - AProgrammer
1
@DanielsaysreinstateMonica,我想知道在util.smartptr.shared/1中,“shared_ptr类模板存储指针,通常通过new获得。shared_ptr实现了共享所有权的语义;指针的最后一个剩余所有者负责销毁对象或以其他方式释放与存储指针相关联的资源。”中“释放与存储指针相关联的资源”并不是针对此目的而设计的。但是控制块也应该存活到最后一个弱指针被删除。 - AProgrammer
显示剩余2条评论

5

来自 std::shared_ptr,我们得到:

控制块是一个动态分配的对象,它包含:

  • 指向托管对象或托管对象本身的指针;
  • 删除器(类型擦除);
  • 分配器(类型擦除);
  • 拥有托管对象的 shared_ptr 数量;
  • 引用托管对象的 weak_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]
通用要求适用于所有make_sharedallocate_sharedmake_shared_default_initallocate_shared_default_init重载函数,除非另有规定,否则如下所述。
备注: (7.1) - 实现不应执行多个内存分配。[注:这提供了与侵入式智能指针等效的效率。-end note] 因此,标准规定std::allocate_shared应该使用Alloc来控制块。

1
抱歉,cppreference不是规范性文本。它是一个很棒的资源,但不一定适合于“语言律师”问题。 - StoryTeller - Unslander Monica
@PaulEvans,http://eel.is/c++draft/util.smartptr.shared.create - AProgrammer
1
然而,这谈论的是make_shared,而不是构造函数本身。尽管如此,我仍然可以使用成员对于小的删除者。 - L. F.
@StoryTeller-UnslanderMonica,cppreference通常很好,比标准文本更冗长,对于理解不同版本的功能变化非常有帮助。但是它在内存模型和原子操作方面非常糟糕和不完整。 - curiousguy
@L.F. 我指的是 std::allocate_sharedstd::make_shared 恰好在同一部分。我理解它的意思是:std::allocate_shared 应该为控制块(因此也是删除器)和 T 对象的内存分配一个 Alloc 分配。 - Paul Evans
显示剩余5条评论

3
我认为这是未指定的。
以下是相关构造函数的规范: [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);

Effects: Constructs a shared_­ptr object that owns the object p and the deleter d. When T is not an array type, the first and second constructors enable shared_­from_­this with p. The second and fourth constructors shall use a copy of a to allocate memory for internal use. If an exception is thrown, d(p) is called.

现在,我的理解是当实现需要内存进行内部使用时,它会使用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在堆栈上分配内存。因此不符合标准要求。 - bartop
1
@bartop 不会在堆栈上分配任何内存。_Smaller_deleter 无条件是 shared_ptr 表示法的一部分。在此空间上调用构造函数并不意味着分配任何东西。否则,即使持有控制块的指针也算是“分配内存”,对吧? :-) - L. F.
但是删除器不需要可复制,那么这怎么工作呢? - Nicol Bolas
@NicolBolas 嗯... 使用 std::move(__d),并在需要复制时回退到 allocate - L. F.

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