3210. allocate_shared is inconsistent about removing const from the pointer passed to allocator construct and destroy

Section: 20.3.2.2.7 [util.smartptr.shared.create] Status: New Submitter: Billy O'Neal III Opened: 2019-05-29 Last modified: 2024-10-02

Priority: 3

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

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

View all issues with New status.

Discussion:

I implemented the fix for LWG 3008 and Stephan pointed out there's an inconsistency here for allocate_shared<const T>.

20.3.2.2.7 [util.smartptr.shared.create] p3 says that the allocator construct call is done without removing cv qualifiers, but 20.3.2.2.7 [util.smartptr.shared.create] p7.12 says that the destroy call is done with removed cv qualifiers.

The fallback for allocator_traits::construct rejects const T* (since it static_casts to void*), so the most likely outcome of attempting to do this today is to fail to compile, which is a break with C++17.

Our options are:

  1. Fix the allocator model to deal with const elements somehow, which breaks compatibility with existing allocators unprepared for const elements here. We would need to extend the allocator requirements to allow const T* to be passed here, and fix our default to const_cast.

  2. Fix allocate_shared to remove const before calling construct, which changes the experience for C++17 customers because allocate_shared constructs a T instead of a const T, but not in a way substantially different to edits P0674 already made here.

  3. Back out allocate_shared's interaction with this part of the allocator model (reverting this part of P0674 and reopening LWG 3008).

  4. Go around the problem by prohibiting allocate_shared<const T>, which breaks existing C++17 customers.

Billy O'Neal argues that only (2) preserves the design intent P0674 while maintaining compatibility for most allocators and most C++17 customers.

Peter Dimov argues that (1) isn't likely to break enough to matter.

[2019-06-16 Priority set to 3 based on reflector discussion]

Previous resolution [SUPERSEDED]:

This wording is relative to N4810.

[Drafting note: As the issue submitter prefers option (2), this is wording for that.]

  1. Modify 20.3.2.2.7 [util.smartptr.shared.create] as indicated:

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

    -2- Requires: […]

    […]

    -7- Remarks:

    1. (7.1) — […]

    2. […]

    3. (7.5) — When a (sub)object of a non-array type U is specified to have an initial value of v, or U(l...), where l... is a list of constructor arguments, allocate_shared shall initialize this (sub)object via the expression

      1. (7.5.1) — allocator_traits<A2>::construct(a2, pv, v) or

      2. (7.5.2) — allocator_traits<A2>::construct(a2, pv, l...)

      respectively, where pv points to storage suitable to hold an object of type remove_cv_t<U> and a2 of type A2 is a rebound copy of the allocator a passed to allocate_shared such that its value_type is remove_cv_t<U>.

[2024-04-13; Jiang An comments and provides improved wording]

The currently proposed resolution is meaningless, because "(allocated) storage suitable to hold an object of type remove_cv_t<U>" is always "storage suitable to hold an object of type U", and vice versa. Also, the current specification doesn't seem to specify the type of pv in the cases of allocator_shared, because pv is merely specified to point some storage instead of an object.

[2024-10-02; will be resolved by issue 3216.]

Proposed resolution:

This wording is relative to N4971.

  1. Modify 20.3.2.2.7 [util.smartptr.shared.create] as indicated:

    [Drafting note: As the issue submitter prefers option (2), this is wording for that.]

    template&lt;class T, ...>
      shared_ptr&lt;T&gt; make_shared(args);
    template&lt;class T, class A, ...>
      shared_ptr&lt;T&gt; allocate_shared(const A& a, args);
    template&lt;class T, ...>
      shared_ptr&lt;T&gt; make_shared_for_overwrite(args);
    template&lt;class T, class A, ...>
      shared_ptr&lt;T&gt; allocate_shared_for_overwrite(const A& a, args);
    

    -2- Preconditions: […]

    […]

    -7- Remarks:

    1. (7.1) — […]

    2. […]

    3. (7.5) — When a (sub)object of a non-array type U is specified to have an initial value of v, or U(l...), where l... is a list of constructor arguments, allocate_shared shall initialize this (sub)object via the expression

      1. (7.5.1) — allocator_traits<A2>::construct(a2, pv, v) or

      2. (7.5.2) — allocator_traits<A2>::construct(a2, pv, l...)

      respectively, where pv has type remove_cv_t<U>* and points to storage suitable to hold an object of type U and a2 of type A2 is a rebound copy of the allocator a passed to allocate_shared such that its value_type is remove_cv_t<U>.

    4. (7.6) — […]

    5. (7.7) — When a (sub)object of non-array type U is specified to have a default initial value, allocate_shared shall initialize this (sub)object via the expression allocator_traits<A2>::construct(a2, pv), where pv has type remove_cv_t<U>* and points to storage suitable to hold an object of type U and a2 of type A2 is a rebound copy of the allocator a passed to allocate_shared such that its value_type is remove_cv_t<U>.