Document Number

P0674R0

Date

2017-06-15

Project

Programming Language C++

Audience

Library Evolution Working Group

Summary

This paper proposes adding array support to make_shared and allocate_shared, via the syntax make_shared<T[]> and make_shared<T[N]>.

Revision History

Changes in Revision 3

This revision of N3939 removes the _noinit overloads, performs some slight modifications to the proposed text, and adds a wording rationale section.

Changes in Revision 2

This revision of N3870 removes the scalar T&& overloads, reflecting the result of the LEWG review.

Changes in Revision 1

This revision of N3641 significantly narrows the scope of the proposal, based on feedback from Jeffrey Yasskin and Stephan T. Lavavej. It cuts down the number of overloads considerably, leaving only two use cases:

  • Value initialization, analogous to new U[N]():

    template<class T> shared_ptr<T> make_shared(size_t N); // T is U[]
    template<class T> shared_ptr<T> make_shared(); // T is U[N]
  • Per-element initialization to a specified value, analogous to the std::vector<U>(N, u) constructor:

    template<class T> shared_ptr<T> make_shared(size_t N, const U& u); // T is U[]
    template<class T> shared_ptr<T> make_shared(const U& u); // T is U[N]

Motivation

make_shared is consistently recommended as the preferred way to create objects managed by shared_ptr. It delivers exception safety, is easier to type, and saves one allocation (and a few bytes in the control block). Not surprisingly, a very common request is for it to support arrays.

An implementation of this proposal has been available from Boost since release 1.56 and is in wide use.

The proposed wording also resolves library issue #2070.

Wording Rationale

The proposed text contains what some may consider redefinitions of the standard terms initialized to and value-initialized, and a lot of seemingly unnecessary complexity.

This is all needed in order to properly initialize arrays due to the irregular behavior of the standard new-expression. When the type T in the expression ::new (pv) T() is an array type, what happens is not, as in the scalar case, an object of type T being constructed at the address pv. When T is for instance U[5], the expression turns into the array form of new, which means that it calls operator new[] instead and may prepend a length prefix to the array elements. The end result is that code cannot assume that after the new-expression pv points to an object of type T, that is, U[5] — because it may not.

For this reason, the wording below does not define initialization as just ::new T. Instead, the array initializations are specifically redefined to consist of initializations of every element. Only for scalar types does the wording invoke ::new T (or construct in the allocator case.)

Proposed Text

(All edits are relative to N4659.)

Change 23.11.2.2 [util.smartptr.shared] p1 as follows:

  • // 23.11.2.2.6, shared_ptr creation
    template<class T, class... Args>
      shared_ptr<T> make_shared(Args&&... args); // T is not array
    template<class T, class A, class... Args>
      shared_ptr<T> allocate_shared(const A& a, Args&&... args); // T is not array
    
    template<class T> shared_ptr<T> make_shared(size_t N); // T is U[]
    template<class T, class A>
      shared_ptr<T> allocate_shared(const A& a, size_t N); // T is U[]
    
    template<class T> shared_ptr<T> make_shared(); // T is U[N]
    template<class T, class A>
      shared_ptr<T> allocate_shared(const A& a); // T is U[N]
    
    template<class T>
      shared_ptr<T> make_shared(size_t N, const remove_extent_t<T>& u); // T is U[]
    template<class T, class A>
      shared_ptr<T> allocate_shared(const A& a, size_t N,
        const remove_extent_t<T>& u); // T is U[]
    
    template<class T> shared_ptr<T>
      make_shared(const remove_extent_t<T>& u); // T is U[N]
    template<class T, class A>
      shared_ptr<T> allocate_shared(const A& a,
        const remove_extent_t<T>& u); // T is U[N]
    

Replace the contents 23.11.2.2.6 [util.smartptr.shared.create] with the following:

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

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

    A shall be an allocator (20.5.3.5).

    Effects:

    Allocates memory for an object of type T (or U[N] when T is U[], where N is determined from args as specified by the concrete overload). The object is initialized from args as specified by the concrete overload. The allocate_shared templates use a copy of a (rebound for an unspecified value_type) to allocate memory. If an exception is thrown, the functions have no effect.

    Returns:

    A shared_ptr instance that stores and owns the address of the newly constructed object.

    Postconditions:

    r.get() != 0 && r.use_count() == 1, where r is the return value.

    Throws:

    bad_alloc, an exception thrown from allocate, or from the initialization of the object.

    Remarks:
    • Implementations should perform no more than one memory allocation. [ Note: This provides efficiency equivalent to an intrusive smart pointer. -- end note ].

    • When an object of an array type U is specified to be initialized to a value u of the same type, this shall be interpreted to mean that each array element of the object is initialized to the corresponding element from u.

    • When an object of an array type is specified to be value-initialized, this shall be interpreted to mean that each array element of the object is value-initialized.

    • When a (sub)object of a non-array type U is specified to be initialized to a value v, or to U(l...), where l... is a list of constructor arguments, make_shared shall perform this initialization via the expression ::new(pv) U(v) or ::new(pv) U(l...) respectively, where pv has type void* and points to storage suitable to hold an object of type U.

    • When a (sub)object of a non-array type U is specified to be initialized to a value v, or to U(l...), where l... is a list of constructor arguments, allocate_shared shall perform this initialization via the expression allocator_traits<A2>::construct(a2, pv, v) or allocator_traits<A2>::construct(a2, pv, l...) respectively, where pv 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 U.

    • When a (sub)object of non-array type U is specified to be value-initialized, make_shared shall perform this initialization via the expression ::new(pv) U(), where pv has type void* and points to storage suitable to hold an object of type U.

    • When a (sub)object of non-array type U is specified to be value-initialized, allocate_shared shall perform this initialization via the expression allocator_traits<A2>::construct(a2, pv), where pv 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 U.

    • Array elements are initialized in ascending order of their addresses.

    • When the lifetime of the object managed by the return value ends, or when the initialization of an array element throws an exception, the initialized elements should be destroyed in the reverse order of their construction.

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

    template<class T, class... Args>
      shared_ptr<T> make_shared(Args&&... args); // T is not array
    template<class T, class A, class... Args>
      shared_ptr<T> allocate_shared(const A& a, Args&&... args); // T is not array
    Returns:

    A shared_ptr to an object of type T, initialized to T(forward<Args>(args)...).

    Remarks:

    These overloads shall only participate in overload resolution when T is not an array type. The shared_ptr constructors called by these functions enable shared_from_this with the address of the newly constructed object of type T.

    [ Example:

    shared_ptr<int> p = make_shared<int>(); // shared_ptr to int()
    shared_ptr<vector<int>> q = make_shared<vector<int>>(16, 1);
      // shared_ptr to vector of 16 elements with value 1

    -- end example ].

    template<class T> shared_ptr<T> make_shared(size_t N); // T is U[]
    template<class T, class A>
      shared_ptr<T> allocate_shared(const A& a, size_t N); // T is U[]
    Returns:

    A shared_ptr to a value-initialized object of type U[N], where U is remove_extent_t<T>.

    Remarks:

    These overloads shall only participate in overload resolution when T is of the form U[].

    [ Example:

    shared_ptr<double[]> p = make_shared<double[]>(1024);
      // shared_ptr to a value-initialized double[1024]
    shared_ptr<double[][2][2]> q = make_shared<double[][2][2]>(6);
      // shared_ptr to a value-initialized double[6][2][2]

    -- end example ].

    template<class T> shared_ptr<T> make_shared(); // T is U[N]
    template<class T, class A>
      shared_ptr<T> allocate_shared(const A& a); // T is U[N]
    Returns:

    A shared_ptr to a value-initialized object of type T.

    Remarks:

    These overloads shall only participate in overload resolution when T is of the form U[N].

    [ Example:

    shared_ptr<double[1024]> p = make_shared<double[1024]>();
      // shared_ptr to a value-initialized double[1024]
    shared_ptr<double[6][2][2]> q = make_shared<double[6][2][2]>();
      // shared_ptr to a value-initialized double[6][2][2]

    -- end example ].

    template<class T>
      shared_ptr<T> make_shared(size_t N, const remove_extent_t<T>& u); // T is U[]
    template<class T, class A>
      shared_ptr<T> allocate_shared(const A& a, size_t N,
        const remove_extent_t<T>& u); // T is U[]
    Returns:

    A shared_ptr to an object of type U[N], where U is remove_extent_t<T> and each array element is initialized to u.

    Remarks:

    These overloads shall only participate in overload resolution when T is of the form U[].

    [ Example:

    shared_ptr<double[]> p = make_shared<double[]>(1024, 1.0);
      // shared_ptr to a double[1024], where each element is 1.0
    shared_ptr<double[][2]> q = make_shared<double[][2]>(6, {1.0, 0.0});
      // shared_ptr to a double[6][2], where each double[2] element is {1.0, 0.0}
    shared_ptr<vector<int>[]> r = make_shared<vector<int>[]>(4, {1, 2});
      // shared_ptr to a vector<int>[4], where each vector has contents {1, 2}

    -- end example ].

    template<class T>
      shared_ptr<T> make_shared(const remove_extent_t<T>& u); // T is U[N]
    template<class T, class A>
      shared_ptr<T> allocate_shared(const A& a,
        const remove_extent_t<T>& u); // T is U[N]
    Returns:

    A shared_ptr to an object of type T, where each array element of type remove_extent_t<T> is initialized to u.

    Remarks:

    These overloads shall only participate in overload resolution when T is of the form U[N].

    [ Example:

    shared_ptr<double[1024]> p = make_shared<double[1024]>(1.0);
      // shared_ptr to a double[1024], where each element is 1.0
    shared_ptr<double[6][2]> q = make_shared<double[6][2]>({1.0, 0.0});
      // shared_ptr to a double[6][2], where each double[2] element is {1.0, 0.0}
    shared_ptr<vector<int>[4]> r = make_shared<vector<int>[4]>({1, 2});
      // shared_ptr to a vector<int>[4], where each vector has contents {1, 2}

    -- end example ].