2070. allocate_shared should use allocator_traits<A>::construct

Section: 20.3.2.2.7 [util.smartptr.shared.create] Status: Resolved Submitter: Jonathan Wakely Opened: 2011-07-11 Last modified: 2017-07-16

Priority: 2

View other active issues in [util.smartptr.shared.create].

View all other issues in [util.smartptr.shared.create].

View all issues with Resolved status.

Discussion:

20.3.2.2.7 [util.smartptr.shared.create] says:

-2- Effects: Allocates memory suitable for an object of type T and constructs an object in that memory via the placement new expression ::new (pv) T(std::forward<Args>(args)...). The template allocate_shared uses a copy of a to allocate memory. If an exception is thrown, the functions have no effect.

This explicitly requires placement new rather than using allocator_traits<A>::construct(a, (T*)pv, std::forward<Args>(args)...) In most cases that would result in the same placement new expression, but would allow more control over how the object is constructed e.g. using scoped_allocator_adaptor to do uses-allocator construction, or using an allocator declared as a friend to construct objects with no public constructors.

[2011-08-16 Bloomington:]

Agreed to fix in principle, but believe that make_shared and allocate_shared have now diverged enough that their descriptions should be separated. Pablo and Stefanus to provide revised wording.

Daniel's (old) proposed resolution:

This wording is relative to the FDIS.

  1. Change the following paragraphs of 20.3.2.2.7 [util.smartptr.shared.create] as indicated (The suggested removal of the last sentence of p1 is not strictly required to resolve this issue, but is still recommended, because it does not say anything new but may give the impression that it says something new):

    template<class T, class... Args> shared_ptr<T> make_shared(Args&&... args);
    template<class T, class A, class... Args>
      shared_ptr<T> allocate_shared(const A& a, Args&&... args);
    

    -1- Requires: For the template make_shared, tThe expression ::new (pv) T(std::forward<Args>(args)...), where pv has type void* and points to storage suitable to hold an object of type T, shall be well formed. For the template allocate_shared, the expression allocator_traits<A>::construct(a, pt, std::forward<Args>(args)...), where pt has type T* and points to storage suitable to hold an object of type T, shall be well formed. A shall be an allocator ([allocator.requirements]). The copy constructor and destructor of A shall not throw exceptions.

    -2- Effects: Allocates memory suitable for an object of type T and constructs an object in that memory. The template make_shared constructs the object via the placement new expression ::new (pv) T(std::forward<Args>(args)...). The template allocate_shared uses a copy of a to allocate memory and constructs the object by calling allocator_traits<A>::construct(a, pt, std::forward<Args>(args)...). If an exception is thrown, the functions have no effect.

    -3- Returns: A shared_ptr instance that stores and owns the address of the newly constructed object of type T.

    -4- Postconditions: get() != 0 && use_count() == 1

    -5- Throws: bad_alloc, or, for the template make_shared, an exception thrown from the constructor of T, or, for the template allocate_shared, an exception thrown from A::allocate or from allocator_traits<A>::constructfrom the constructor of T.

    -6- Remarks: Implementations are encouraged, but not required, to perform no more than one memory allocation. [ Note: This provides efficiency equivalent to an intrusive smart pointer. — end note ]

    -7- [ Note: These functions will typically allocate more memory than sizeof(T) to allow for internal bookkeeping structures such as the reference counts. — end note ]

[2011-12-04: Jonathan and Daniel improve wording]

See also c++std-lib-31796

[2013-10-13, Ville]

This issue is related to 2089.

[2014-02-15 post-Issaquah session : move to Tentatively NAD]

STL: This takes an allocator, but then ignores its construct. That's squirrely.

Alisdair: The convention is when you take an allocator, you use its construct.

STL: 23.2.2 [container.requirements.general]/3, argh! This fills me with despair, but I understand it now.

STL: Ok, this is some cleanup.

STL: You're requiring b to be of type A and not being rebound, is that an overspecification?

Pablo: Good point. Hmm, that's only a requirement on what must be well-formed.

STL: If it's just a well-formed requirement, then why not just use a directly?

Pablo: Yeah, the well-formed requirement is overly complex. It's not a real call, we could just use a directly. It makes it harder to read.

Alisdair: b should be an allocator in the same family as a.

Pablo: This is a well-formed requirement, I wonder if it's the capital A that's the problem here. It doesn't matter here, this is way too much wording.

Alisdair: It's trying to tie the constructor arguments into the allocator requirements.

Pablo: b could be struck, that's a runtime quality. The construct will work with anything that's in the family of A.

Alisdair: The important part is the forward of Args.

Pablo: A must be an allocator, and forward Args must work with that.

Alisdair: First let's nail down A.

Pablo: Then replace b with a, and strike the rest.

STL: You need pt's type, at least.

Pablo: There's nothing to be said about runtime constraints here, this function doesn't even take a pt.

STL: Looking at the Effects, I believe b is similarly messed up, we can use a2 to construct an object.

Alisdair: Or any allocator in the family of a.

STL: We say this stuff for the deallocate too, it should be lifted up.

STL: "owns the address" is weird.

Alisdair: shared_ptr owns pointers, although it does sound funky.

Walter: "to destruct" is ungrammatical.

STL: "When ownership is given up" is not what we usually say.

Alisdair: I think the Returns clause is the right place to say this.

STL: The right place to say this is shared_ptr's dtor, we don't want to use Core's "come from" convention.

Alisdair: I'm on the hook to draft cleaner wording.

[2015-10, Kona Saturday afternoon]

AM: I was going to clean up the wording, but haven't done it yet.
Defer until we have new wording.

[2016-03, Jacksonville]

Alisdair: we need to figure out whether we should call construct or not; major implementation divergence
STL: this does not grant friendship, does it?
Jonathan: some people want it.
Thomas: scoped allocator adapter should be supported, so placement new doesn't work
Alisdair: this makes the make_ functions impossible
Thomas: you don't want to use those though.
Alisdair: but people use that today, at Bloomberg
Alisdair: and what do we do about fancy pointers?
Jonathan: we constrain it to only non-fancy pointers.
STL: shared_ptr has never attempted to support fancy pointers; seems like a paper is needed.
Poll: call construct:6 operator new: 0 don't care: 4
Poll: should we support fancy pointers? Yes: 1 No: 4 don't care: 4
STL: 20.8.2.2.6p2: 'and pv->~T()' is bogus for void
STL: 20.8.2.2.6p4: is this true even if we're going to allocate a bit more?
Alisdair: yes
Alisdair: coming up with new wording

[2016-08, Chicago Monday PM]

Alisdair to provide new wording this week

[2017-07 Toronto]

Resolved by the adoption of P0674R1 in Toronto

Proposed resolution:

This wording is relative to the FDIS.

  1. Change the following paragraphs of 20.3.2.2.7 [util.smartptr.shared.create] as indicated:

    template<class T, class... Args> shared_ptr<T> make_shared(Args&&... args);
    template<class T, class A, class... Args>
      shared_ptr<T> allocate_shared(const A& a, Args&&... args);
    

    -1- Requires: The expression ::new (pv) T(std::forward<Args>(args)...), where pv has type void* and points to storage suitable to hold an object of type T, shall be well formed. A shall be an allocator (16.4.4.6 [allocator.requirements]). The copy constructor and destructor of A shall not throw exceptions.

    -2- Effects: Equivalent to

     
    return allocate_shared<T>(allocator<T>(), std::forward<Args>(args)...);
    

    Allocates memory suitable for an object of type T and constructs an object in that memory via the placement new expression ::new (pv) T(std::forward<Args>(args)...). The template allocate_shared uses a copy of a to allocate memory. If an exception is thrown, the functions have no effect.

    -?- Remarks: An implementation may meet the effects (and the implied guarantees) without creating the allocator object [Note: That is, user-provided specializations of std::allocator may not be instantiated, the expressions ::new (pv) T(std::forward<Args>(args)...) and pv->~T() may be evaluated directly — end note].

    -3- Returns: A shared_ptr instance that stores and owns the address of the newly constructed object of type T.

    -4- Postconditions: get() != 0 && use_count() == 1

    -5- Throws: bad_alloc, or an exception thrown from A::allocate or from the constructor of T.

    -6- Remarks: Implementations are encouraged, but not required, to perform no more than one memory allocation. [Note: This provides efficiency equivalent to an intrusive smart pointer. — end note]

    -7- [Note: These functions will typically allocate more memory than sizeof(T) to allow for internal bookkeeping structures such as the reference counts. — end note]

  2. Add the following set of new paragraphs immediately following the previous paragraph 7 of 20.3.2.2.7 [util.smartptr.shared.create]:

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

    -?- Requires: The expressions allocator_traits<A>::construct(b, pt, std::forward<Args>(args)...) and allocator_traits<A>::destroy(b, pt) shall be well-formed and well-defined, where b has type A and is a copy of a and where pt has type T* and points to storage suitable to hold an object of type T. A shall meet the allocator requirements (16.4.4.6 [allocator.requirements]).

    -?- Effects: Uses an object a2 of type allocator_traits<A>::rebind_alloc<unspecified> that compares equal to a to allocate memory suitable for an object of type T. Uses a copy b of type A from a to construct an object of type T in that memory by calling allocator_traits<A>::construct(b, pt, std::forward<Args>(args)...). If an exception is thrown, the function has no effect.

    -?- Returns: A shared_ptr instance that stores and owns the address of the newly constructed object of type T. When ownership is given up, the effects are as follows: Uses a copy b2 of type A from a to destruct an object of type T by calling allocator_traits<A>::destroy(b2, pt2) where pt2 has type T* and refers to the newly constructed object. Then uses an object of type allocator_traits<A>::rebind_alloc<unspecified> that compares equal to a to deallocate the allocated memory.

    -?- Postconditions: get() != 0 && use_count() == 1

    -?- Throws: Nothing unless memory allocation or allocator_traits<A>::construct throws an exception.

    -?- Remarks: Implementations are encouraged, but not required, to perform no more than one memory allocation. [Note: Such an implementation provides efficiency equivalent to an intrusive smart pointer. — end note]

    -?- [Note: This function will typically allocate more memory than sizeof(T) to allow for internal bookkeeping structures such as the reference counts. — end note]