Is a shared_ptr's deleter stored in memory allocated by the custom allocator?
Asked Answered
N

3

23

Say I have a shared_ptr with a custom allocator and a custom deleter.

I can't find anything in the standard that talks about where the deleter should be stored: it doesn't say that the custom allocator will be used for the deleter's memory, and it doesn't say that it won't be.

Is this unspecified or am I just missing something?

Neisa answered 19/11, 2019 at 11:25 Comment(0)
P
12

util.smartptr.shared.const/9 in C++ 11:

Effects: Constructs a shared_ptr object that owns the object p and the deleter d. The second and fourth constructors shall use a copy of a to allocate memory for internal use.

The second and fourth constructors have these prototypes:

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);

In the latest draft, util.smartptr.shared.const/10 is equivalent for our purpose:

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.

So the allocator is used if there is a need to allocate it in allocated memory. Based on the current standard and at relevant defect reports, allocation is not mandatory but assumed by the committee.

  • Although the interface of shared_ptr allows an implementation where there is never a control block and all shared_ptr and weak_ptr are put in a linked list, there is no such implementation in practice. Additionally, the wording has been modified assuming, for instance, that the use_count is shared.

  • The deleter is required to only move constructible. Thus, it is not possible to have several copies in the shared_ptr.

One can imagine an implementation which puts the deleter in a specially designed shared_ptr and moves it when it the special shared_ptr is deleted. While the implementation seems conformant, it is also strange, especially since a control block may be needed for the use count (it is perhaps possible but even weirder to do the same thing with the use count).

Relevant DRs I found: 545, 575, 2434 (which acknowledge that all implementations are using a control block and seem to imply that multi-threading constraints somewhat mandate it), 2802 (which requires that the deleter only move constructible and thus prevents implementation where the deleter is copied between several shared_ptr's).

Pricillaprick answered 19/11, 2019 at 11:48 Comment(7)
"to allocate memory for internal use" What if the implementation isn't going to allocate memory for internal use to begin with? It can use a member.Decontrol
@L.F. It can't, the interface does not allow for that.Pricillaprick
Theoretically, it can still use some kind of "small deleter optimization", right?Decontrol
What is weird is that I can't find anything about using the same allocator (copy of a) to deallocate that memory. Which would imply some storage of that copy of a. There is no information about it in [util.smartptr.shared.dest].Broadbrim
@L.F. It could be but I'm not sure. I'd have to think more about the interaction with shared_from_this. It would for sure not be practical, the facts that the interface expose use_count and the standard evolved to remove the note that use_count can be inefficient, that the only use of the allocator is to allocate a control block, and so on hint strongly that there is a control block and putting some information out of control block when one is available seems weird.Pricillaprick
@DanielsaysreinstateMonica, I agree that's weird. I can't find it either.Pricillaprick
@DanielsaysreinstateMonica, I wonder if in util.smartptr.shared/1: "The shared_­ptr class template stores a pointer, usually obtained via new. shared_­ptr implements semantics of shared ownership; the last remaining owner of the pointer is responsible for destroying the object, or otherwise releasing the resources associated with the stored pointer." the releasing the resources associated with the stored pointer is not intended for that. But the control block should survive as well until the last weak pointer is deleted.Pricillaprick
S
5

From std::shared_ptr we have:

The control block is a dynamically-allocated object that holds:

  • either a pointer to the managed object or the managed object itself;
  • the deleter (type-erased);
  • the allocator (type-erased);
  • the number of shared_ptrs that own the managed object;
  • the number of weak_ptrs that refer to the managed object.

And from std::allocate_shared we get:

template< class T, class Alloc, class... Args >
shared_ptr<T> allocate_shared( const Alloc& alloc, Args&&... args );

Constructs an object of type T and wraps it in a std::shared_ptr [...] in order to use one allocation for both the control block of the shared pointer and the T object.

So it looks like std::allocate_shared should allocate the deleter with your Alloc.

EDIT: And from n4810 §20.11.3.6 Creation [util.smartptr.shared.create]

1 The common requirements that apply to all make_shared, allocate_shared, make_shared_default_init, and allocate_shared_default_init overloads, unless specified otherwise, are described below.

[...]

7 Remarks: (7.1) — Implementations should perform no more than one memory allocation. [Note: This provides efficiency equivalent to an intrusive smart pointer. —end note]

[Emphasis all mine]

So the standard is saying that std::allocate_shared should use Alloc for the control block.

Scharaga answered 19/11, 2019 at 11:57 Comment(10)
I'm sorry by cppreference is not a normative text. It's a great resource, but not necessarily for language-lawyer questions.Coccidioidomycosis
@StoryTeller-UnslanderMonica Totally agree - looked through the latest standard and couldn't find anything so went with cppreference.Scharaga
@PaulEvans, eel.is/c++draft/util.smartptr.shared.createPricillaprick
However, this is talking about make_shared, not the constructors themselves. Still, I can use a member for small deleters.Decontrol
@L.F. It's saying that implementations should use one memory allocation for the control block of the shared pointer and the T object. Since it's allocating for the T object, Alloc must be used for std::allocate_shared.Scharaga
@StoryTeller-UnslanderMonica cppreference is good in general, and bloated than the std text, and excellent for understanding the variations in a feature over versions. But it's very poor and incomplete re: memory model and atomics.Mugwump
@PaulEvans But that doesn't mean that make_shared has to be implemented in terms of the publicly available constructors, right?Decontrol
@L.F. I'm referring to std::allocate_shared, std::make_shared just so happens to be in the same section. I read it as saying: std::allocate_shared should allocate memory for the control block (and therefore the deleter) and the T object's memory in one Alloc allocation.Scharaga
@PaulEvans Sorry, both of my comments were meant to say allocate_shared instead of make_shared. What if I don't use allocate_shared and use the constructor directly?Decontrol
@L.F. That's where I'm finding it very difficult to get anything hard from the standard.Scharaga
D
3

I believe this is unspecified.

Here's the specification of the relevant constructors: [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.

Now, my interpretation is that when the implementation needs memory for internal use, it does so by using a. It doesn't mean that the implementation has to use this memory to place everything. For example, suppose that there's this weird implementation:

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
    }
// ...
};

Does this implementation "use a copy of a to allocate memory for internal use"? Yes, it does. It never allocates memory except by using a. There are many problems with this naive implementation, but let's say that it switches to using allocators in all but the simplest case in which the shared_ptr is constructed directly from a pointer and is never copied or moved or otherwise referenced and there are no other complications. The point is, just because we fail to imagine a valid implementation doesn't by itself prove that it cannot theoretically exist. I am not saying that such an implementation can actually be found in the real world, just that the standard doesn't seem to be actively prohibiting it.

Decontrol answered 19/11, 2019 at 12:23 Comment(4)
IMO your shared_ptr for small types allocates memory on the stack. And so does not meet standard requirementsNumber
@Number It doesn’t “allocate” any memory on the stack. _Smaller_deleter is unconditionally a part of the representation of a shared_ptr. Calling a constructor on this space doesn’t mean allocating anything. Otherwise, even holding a pointer to the control block counts as “allocating memory”, right? :-)Decontrol
But the deleter is not required to be copyable, so how would this work?Appointed
@NicolBolas Umm ... Use std::move(__d), and fall back to allocate when copy is required.Decontrol

© 2022 - 2024 — McMap. All rights reserved.