Document Number: N3939
Programming Language C++
Library Working Group
 
Peter Dimov, <pdimov@pdimov.com>
Glen Fernandes, <glenfe@live.com>
 
2014-02-17

Extending make_shared to Support Arrays, Revision 2

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

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 make_shared 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

Programmers like make_shared. 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.

Boost release 1.53, available from www.boost.org, contains an implementation of an earlier revision of this proposal (N3641). The Boost distribution contains tests and documentation, which can be browsed online.

The upcoming Boost release 1.56 will contain an implementation of this proposal, currently available from Github.

A shared_ptr that supports arrays is a dependency that is the subject of a separate paper (N3920).

The proposed wording also resolves library issue #2070.

Rationale

make_shared_noinit

It is not uncommon for arrays of built-in types such as unsigned char or double to be immediately initialized by the user in their entirety after allocation. In these cases, the value initialization performed by make_shared is redundant and hurts performance, and a way to choose default initialization is needed.

This proposal suggests make_shared_noinit and allocate_shared_noinit as a way to perform default initialization on the elements. The suffix _noinit, instead of something derived from "default", has been chosen because the use cases of this overload always deal with either uninitialized elements (when the type is a built-in) or with potentially uninitialized elements (when the type is dependent on a template parameter in a generic function and may be a built-in). Typically, therefore, the programmer assumes that after make_shared_noinit, the elements are uninitialized, that is, hold unspecified values, and the _noinit name reflects this assumption.

Proposed Text

(All edits are relative to N3485.)

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

// 20.7.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 U& u); // T is U[]
template<class T, class A> shared_ptr<T> allocate_shared(const A& a, size_t N, const U& u); // T is U[]

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

template<class T> shared_ptr<T> make_shared_noinit(); // T is not U[]
template<class T, class A> shared_ptr<T> allocate_shared_noinit(const A& a); // T is not U[]

template<class T> shared_ptr<T> make_shared_noinit(size_t N); // T is U[]
template<class T, class A> shared_ptr<T> allocate_shared_noinit(const A& a, size_t N); // T is U[]

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

The common requirements that apply to all make_shared, allocate_shared, make_shared_noinit, and allocate_shared_noinit 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);
template<class T, ...> shared_ptr<T> make_shared_noinit(args);
template<class T, class A, ...> shared_ptr<T> allocate_shared_noinit(const A& a, args);
Requires: A shall be an allocator (17.6.3.5). The copy constructor and destructor of A shall not throw exceptions.
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 templates allocate_shared and allocate_shared_noinit use a copy of a 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 A::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 of the same type u, 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 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.
When a (sub)object of non-array type U is specified to be default-initialized, make_shared_noinit and allocate_shared_noinit 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.
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.
[ 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].
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 U[N].
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 U& u); // T is U[]
template<class T, class A> shared_ptr<T> allocate_shared(const A& a, size_t N, const U& u); // T is U[]
Returns: A shared_ptr to an object of type U[N], where each array element of type U 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 U& u); // T is U[N]
template<class T, class A> shared_ptr<T> allocate_shared(const A& a, const U& u); // T is U[N]
Returns: A shared_ptr to an object of type U[N], where each array element of type U 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 ].
template<class T> shared_ptr<T> make_shared_noinit(); // T is not U[]
template<class T, class A> shared_ptr<T> allocate_shared_noinit(const A& a); // T is not U[]
Returns: A shared_ptr to a default-initialized object of type T.
Remarks: These overloads shall only participate in overload resolution when T is not of the form U[].
[ Example:
  struct X { double data[1024]; };
  shared_ptr<X> p = make_shared_noinit<X>(); // shared_ptr to a default-initialized X, with X::data left uninitialized
  shared_ptr<double[1024]> q = make_shared_noinit<double[1024]>(); // shared_ptr to a default-initialized double[1024], with the elements left unitialized
— end example ].
template<class T> shared_ptr<T> make_shared_noinit(size_t N); // T is U[]
template<class T, class A> shared_ptr<T> allocate_shared_noinit(const A& a, size_t N); // T is U[]
Returns: A shared_ptr to a default-initialized object of type U[N].
Remarks: These overloads shall only participate in overload resolution when T is of the form U[].
[ Example:
  shared_ptr<double[]> p = make_shared_noinit<double[]>(1024); // shared_ptr to a default-initialized double[1024], with the elements left unitialized
— end example ].

— end