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?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055

3 Answers3

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

AProgrammer
  • 51,233
  • 8
  • 91
  • 143
  • 2
    "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. – L. F. Nov 19 '19 at 11:55
  • 1
    @L.F. It can't, the interface does not allow for that. – AProgrammer Nov 19 '19 at 11:59
  • Theoretically, it can still use some kind of "small deleter optimization", right? – L. F. Nov 19 '19 at 12:01
  • 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]. – Daniel Langr Nov 19 '19 at 12:11
  • @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. – AProgrammer Nov 19 '19 at 12:15
  • @DanielsaysreinstateMonica, I agree that's weird. I can't find it either. – AProgrammer Nov 19 '19 at 12:19
  • 1
    @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. – AProgrammer Nov 19 '19 at 12:35
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.

Paul Evans
  • 27,315
  • 3
  • 37
  • 54
  • 1
    I'm sorry by cppreference is not a normative text. It's a great resource, but not necessarily for [tag:language-lawyer] questions. – StoryTeller - Unslander Monica Nov 19 '19 at 11:58
  • @StoryTeller-UnslanderMonica Totally agree - looked through the latest standard and couldn't find anything so went with cppreference. – Paul Evans Nov 19 '19 at 12:00
  • @PaulEvans, http://eel.is/c++draft/util.smartptr.shared.create – AProgrammer Nov 19 '19 at 12:20
  • 1
    However, this is talking about `make_shared`, not the constructors themselves. Still, I can use a member for small deleters. – L. F. Nov 19 '19 at 12:27
  • @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`. – Paul Evans Nov 19 '19 at 14:12
  • @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. – curiousguy Nov 19 '19 at 15:43
  • @PaulEvans But that doesn't mean that `make_shared` has to be implemented in terms of the publicly available constructors, right? – L. F. Nov 20 '19 at 09:44
  • @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. – Paul Evans Nov 20 '19 at 10:42
  • @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? – L. F. Nov 20 '19 at 11:33
  • @L.F. That's where I'm finding it very difficult to get anything hard from the standard. – Paul Evans Nov 20 '19 at 13:57
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.

L. F.
  • 19,445
  • 8
  • 48
  • 82
  • IMO your `shared_ptr` for small types allocates memory on the stack. And so does not meet standard requirements – bartop Nov 19 '19 at 12:56
  • 1
    @bartop 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? :-) – L. F. Nov 19 '19 at 13:11
  • But the deleter is not required to be copyable, so how would this work? – Nicol Bolas Nov 19 '19 at 14:43
  • @NicolBolas Umm ... Use `std::move(__d)`, and fall back to `allocate` when copy is required. – L. F. Nov 20 '19 at 09:45