【问题标题】:Is a shared_ptr's deleter stored in memory allocated by the custom allocator?shared_ptr 的删除器是否存储在自定义分配器分配的内存中?
【发布时间】:2020-03-14 21:51:02
【问题描述】:

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

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

这是未指定还是我只是错过了什么?

【问题讨论】:

    标签: c++ language-lawyer c++17 shared-ptr allocator


    【解决方案1】:

    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_ptrweak_ptr都放在一个链表中,但实际上并没有这样的实现。此外,假设use_count 是共享的,措辞已经过修改。

    • 删除器只需要移动可构造的。因此,shared_ptr 中不可能有多个副本。

    可以想象这样一种实现,它将删除器放在专门设计的shared_ptr 中,并在删除特殊的shared_ptr 时移动它。虽然实现看起来是一致的,但它也很奇怪,特别是因为使用计数可能需要一个控制块(用使用计数做同样的事情也许是可能的,但更奇怪)。

    我发现的相关 DR:5455752434(它们承认所有实现都使用控制块,并且似乎暗示多线程约束在某种程度上要求它)、2802(这需要删除器只能移动可构造的,因此会阻止在多个shared_ptr 之间复制删除器的实现。

    【讨论】:

    • "分配内存供内部使用" 如果实现不打算分配内存供内部使用开始怎么办?它可以使用一个成员。
    • @L.F.不能,界面不允许这样做。
    • 理论上还是可以使用某种“小删除器优化”的吧?
    • 奇怪的是,我找不到任何关于使用相同分配器(a 的副本)来释放该内存的任何信息。这意味着对a 的副本进行了一些存储。 [util.smartptr.shared.dest] 中没有关于它的信息。
    • @DanielsaysreinstateMonica,我想知道是否在 util.smartptr.shared/1 中:“shared_ptr 类模板存储一个指针,通常通过 new 获得。shared_ptr 实现共享所有权的语义;最后剩下的所有者指针负责销毁对象,或者以其他方式释放与存储的指针相关的资源。” 释放与存储的指针相关联的资源 并不是为此而设计的。但是控制块也应该存在,直到最后一个弱指针被删除。
    【解决方案2】:

    来自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]

    1 适用于所有make_sharedallocate_sharedmake_shared_default_init、 和allocate_shared_default_init 重载,除非另有说明,否则如下所述。

    [...]

    7 备注: (7.1) — 实现应该执行不超过一次的内存分配。 [注:这提供 效率相当于一个侵入式智能指针。 ——尾注]

    [强调我的全部]

    所以标准是说std::allocate_shared应该使用Alloc作为控制块。

    【讨论】:

    • 很抱歉,cppreference 不是规范性文本。这是一个很好的资源,但不一定适用于language-lawyer 问题。
    • @StoryTeller-UnslanderMonica 完全同意 - 查看了最新标准,但找不到任何内容,因此使用 cppreference。
    • 但是,这是在谈论make_shared,而不是构造函数本身。不过,我可以将成员用于小型删除器。
    • @L.F.也就是说,实现应该为共享指针的控制块和T 对象分配一个内存。由于它是为T 对象分配的,所以Alloc 必须用于std::allocate_shared
    【解决方案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);
    

    效果: 构造一个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 是直接从指针构造的,从不复制、移动或以其他方式引用,没有其他并发症。关键是,仅仅因为我们无法想象一个有效的实现本身并不能证明它在理论上不存在。我并不是说这样的实现实际上可以在现实世界中找到,只是标准似乎并没有积极禁止它。

    【讨论】:

    • IMO 你的 shared_ptr 对于小类型在堆栈上分配内存。所以不符合标准要求
    • @bartop 它不会在堆栈上“分配”任何内存。 _Smaller_deleter 无条件地是 shared_ptr 表示的一部分。在这个空间上调用构造函数并不意味着分配任何东西。否则,即使持有指向控制块的指针也算作“分配内存”,对吧? :-)
    • 但是删除器不需要是可复制的,那么这将如何工作?
    • @NicolBolas Umm ... 使用std::move(__d),并在需要复制时回退到allocate
    猜你喜欢
    • 1970-01-01
    • 2015-09-30
    • 1970-01-01
    • 1970-01-01
    • 2012-05-29
    • 2019-08-11
    • 2015-03-25
    • 2012-02-22
    • 1970-01-01
    相关资源
    最近更新 更多