This paper proposes adding array support to make_shared
, via the syntax
make_shared<T[]>
and make_shared<T[N]>
.
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.
The proposed changes have been implemented in Boost release 1.53, available from www.boost.org. The Boost distribution contains tests and documentation, which can be browsed online. The Boost implementation is primarily the work of Glen Fernandes.
A shared_ptr
that supports arrays is a dependency that has been proposed
in a separate paper (N3640).
shared_ptr<T[]> p = make_shared<T[]>(N);
shared_ptr<T[N]> q = make_shared<T[N]>();
Creates an array ofN
value-initialized elements of typeT
.
shared_ptr<T[][M]> p = make_shared<T[][M]>(N);
shared_ptr<T[N][M]> q = make_shared<T[N][M]>();
Creates a two-dimensional array ofN*M
value-initialized elements of typeT
. More than two dimensions are supported as well.
shared_ptr<T[]> p = make_shared<T[]>(N, a, b);
shared_ptr<T[N]> q = make_shared<T[N]>(a, b);
Creates an array ofN
elements of typeT
, each initialized toT(a, b)
.
shared_ptr<T[][M]> p = make_shared<T[][M]>(N, a, b);
shared_ptr<T[N][M]> q = make_shared<T[N][M]>(a, b);
Creates a two-dimensional array ofN*M
elements of typeT
, each initialized toT(a, b)
. More than two dimensions are supported as well.
shared_ptr<T[]> p = make_shared_noinit<T[]>(N);
shared_ptr<T[N]> q = make_shared_noinit<T[N]>();
Creates an array ofN
default-initialized elements of typeT
. Useful whenT
is a built-in type such asdouble
. Multidimensional arrays are supported. The scalar version ofmake_shared
is also extended to support this syntax.
shared_ptr<T[]> p = make_shared<T[]>(N, {a, b});
shared_ptr<T[N]> q = make_shared<T[N]>({a, b});
Creates an array ofN
elements of typeT
, each initialized to{a, b}
. Can be used wheneverT{a, b}
is valid, but is particularly useful whenT(a, b)
andT{a, b}
mean different things, such as whenT
is an aggregate or a container. The scalar version ofmake_shared
is also extended to support this syntax.
shared_ptr<int[][3]> p = make_shared<int[][3]>(N, {1, 2, 3});
shared_ptr<int[N][3]> q = make_shared<int[N][3]>({1, 2, 3});
Creates an array ofN
elements of typeint[3]
, each initialized to{1, 2, 3}
. Conceptually the same as the previous example, withT = int[3]
.
shared_ptr<int[]> p = make_shared<int[]>({1, 2, 3});
shared_ptr<int[3]> q = make_shared<int[3]>({1, 2, 3});
Creates an array of 3 elements of type int
, initialized to 1, 2, 3 respectively.
The array size is deduced in the first case, checked in the second.
shared_ptr<int[][3]> p = make_shared<int[][3]>({{1, 2, 3}, {4, 5, 6}});
shared_ptr<int[2][3]> q = make_shared<int[2][3]>({{1, 2, 3}, {4, 5, 6}});
Creates an array of 2 elements of typeint[3]
, initialized to{1, 2, 3}
and{4, 5, 6}
respectively. The outermost array size is deduced in the first case, checked in the second. This is the same as the previous example, withint
replaced withint[3]
.
The expressions make_shared<X[4]>()
and make_shared<X[]>(4)
cannot simply use new(pv) X[4]
to initialize the elements, even though
only value initialization is required. When X
has a destructor, the new[]
expression inserts a hidden size prefix, so that the elements are shifted in memory
and pv
doesn't point to the first element of type X
.
This can be demonstrated by the following program:
#include <memory> #include <iostream> struct X { int v; X(): v(1) {} ~X() {} }; int main() { std::shared_ptr<X[4]> p = std::make_shared<X[4]>(); for( int i = 0; i < 4; ++i ) { std::cout << (*p)[i].v << ' '; } std::cout << std::endl; }
Under one popular implementation, using the stock standard library, the above compiles and works, and produces the following output:
4 1 1 1
whereas, of course, one would expect
1 1 1 1
to be printed instead.
For that reason, the implementation of make_shared
for arrays needs to perform a loop
and initialize the elements one by one, as in:
for( int i = 0; i < N; ++i ) { ::new( (void*)(px+i) ) X(); }
(with exception handling omitted for brevity.)
With this loop in place, extending make_shared
to support constructor arguments is obviously cost-free:
for( int i = 0; i < N; ++i ) { ::new( (void*)(px+i) ) X(args...); }
This is why this proposal does not limit make_shared
to value initialization.
For the same reasons outlined in the previous section, the expression make_shared<Y[4][2]>()
cannot use the loop
for( int i = 0; i < 4; ++i ) { ::new( (void*)(px+i) ) Y[2](); }
for initialization, which is what will happen if the specification and the implementation
do not take into account the possibility of the array element in X[4]
being or an array type (Y[2]
) itself.
So, multidimensional arrays need to be addressed explicitly in the specification, one way or another. The two options are to either disallow them outright, or support them. This proposal chooses to fully support multidimensional arrays, by flattening the initialization loop. For the above example, the initialization would have the form:
for( int i = 0; i < 4*2; ++i ) { ::new( (void*)(py+i) ) Y(); }
Per-element construction using constructor arguments — T(a, b)
— is enough
for the majority of cases, but there are scenarios in which one would like to use
T{a, b}
instead. Aggregates, as one example, can only be initialized
with {}
; standard containers, as another, support both T(a, b)
and T{a, b}
, but give the two forms different meanings.
This proposal suggests the syntax make_shared<T>({a, b})
, make_shared<T[]>(N,
{a, b})
and make_shared<T[N]>({a, b})
as a way to
initialize elements with T{a, b}
for the scalar case, array with an
unknown bound case, and array with known bound case, respectively.
If Args&&... args
could take {a, b}
and forward
it perfectly, we wouldn't need another overload to support the above syntax. It
cannot, so we do. Its implementation is essentially the same as that of the Args
overload; the only difference is that it takes an argument of type remove_all_extents<T>::type&&
and passes it to the constructor instead of args...
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 build-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 uinitialized, that is, hold unspecified values, and the _noinit
name reflects this assumption.
The motivation for providing a way to initialize an entire array from given values comes from the scalar case. Consider the following example, which is supported:
struct X { int v[3][2]; }; make_shared<X>( {{1, 2}, {3, 4}, {5, 6}} );
and now compare with its equivalent without the wrapper struct:
make_shared<int[3][2]>( {{1, 2}, {3, 4}, {5, 6}} );
Clearly, it makes sense for the latter to be supported, since the former is.
Finally, consider the following supported example:
struct X { double a, b; }; make_shared<X[]>( 1024, {1.0, 0.0} );
and its two-dimensional array equivalent:
make_shared<double[][2]>( 1024, {1.0, 0.0} );
Here it also makes perfect sense by analogy for the latter to be supported, since the former is.
(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(T&& t); // T is not array template<class T, class A> shared_ptr<T> allocate_shared(const A& a, T&& t); // T is not array template<class T, class... Args> shared_ptr<T> make_shared(size_t N, Args&&... args); // T is U[] template<class T, class A, class... Args> shared_ptr<T> allocate_shared(const A& a, size_t N, Args&&... args); // T is U[] template<class T, class... Args> shared_ptr<T> make_shared(Args&&... args); // T is U[N] template<class T, class A, class... Args> shared_ptr<T> allocate_shared(const A& a, Args&&... args); // T is U[N] template<class T> shared_ptr<T> make_shared(size_t N, typename remove_all_extents<T>::type&& e); // T is U[] template<class T, class A> shared_ptr<T> allocate_shared(const A& a, size_t N, typename remove_all_extents<T>::type&& e); // T is U[] template<class T> shared_ptr<T> make_shared(typename remove_all_extents<T>::type&& e); // T is U[N] template<class T, class A> shared_ptr<T> allocate_shared(const A& a, typename remove_all_extents<T>::type&& e); // T is U[N] template<class T> shared_ptr<T> make_shared(initializer_list<U> list); // T is U[] template<class T, class A> shared_ptr<T> allocate_shared(const A& a, initializer_list<U> list); // T is U[] template<class T> shared_ptr<T> make_shared(const T& list); // T is U[N] template<class T, class A> shared_ptr<T> allocate_shared(const A& a, const T& list); // T is U[N] template<class T> shared_ptr<T> make_shared(size_t N, const U (&list)[M]); // T is U[][M] template<class T, class A> shared_ptr<T> allocate_shared(const A& a, size_t N, const U (&list)[M]); // T is U[][M] template<class T> shared_ptr<T> make_shared(const U (&list)[M]); // T is U[N][M] template<class T, class A> shared_ptr<T> allocate_shared(const A& a, const U (&list)[M]); // T is U[N][M] template<class T> shared_ptr<T> make_shared_noinit(); // T is not array template<class T, class A> shared_ptr<T> allocate_shared_noinit(const A& a); // T is not array 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[] template<class T> shared_ptr<T> make_shared_noinit(); // T is U[N] template<class T, class A> shared_ptr<T> allocate_shared_noinit(const A& a); // T is U[N]
Replace the contents 20.7.2.2.6 [util.smartptr.shared.create] with the following:
The common requirements that apply to allmake_shared
,allocate_shared
,make_shared_noinit
, andallocate_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 ofA
shall not throw exceptions.Effects: Allocates memory for an object of typeT
(orU[N]
whenT
isU[]
, whereN
is determined fromargs
as specified by the concrete overload). The object is initialized fromargs
as specified by the concrete overload. The templatesallocate_shared
andallocate_shared_noinit
use a copy ofa
to allocate memory. If an exception is thrown, the functions have no effect.Returns: Ashared_ptr
instance that stores and owns the address of the newly constructed object.Postconditions:r.get() != 0 && r.use_count() == 1
, wherer
is the return value.Throws:bad_alloc
, an exception thrown fromA::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 typeU
is specified to be initialized to a value of the same typeu
, this shall be interpreted to mean that each array element of the object is initialized to the corresponding element fromu
.When a (sub)object of typeU
is specified to be initialized to a valuev
, or toU(l...)
, wherel...
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, wherepv
has typevoid*
and points to storage suitable to hold an object of typeU
.When a (sub)object of typeU
is specified to be initialized to a valuev
, or toU(l...)
, wherel...
is a list of constructor arguments,allocate_shared
shall perform this initialization via the expressionallocator_traits<A2>::construct(a2, pv, v)
orallocator_traits<A2>::construct(a2, pv, l...)
respectively, wherepv
points to storage suitable to hold an object of typeU
anda2
of typeA2
is a rebound copy of the allocatora
passed toallocate_shared
such that itsvalue_type
isU
.When a (sub)object of typeU
is specified to be default-initialized,make_shared_noinit
andallocate_shared_noinit
shall perform this initialization via the expression::new(pv) U
, wherepv
has typevoid*
and points to storage suitable to hold an object of typeU
.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 thansizeof(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 arrayReturns: Ashared_ptr
to an object of typeT
, initialized toT(forward<Args>(args)...)
.Remarks: These overloads shall only participate in overload resolution whenT
is not an array type.[ Example:shared_ptr<vector<int>> p = 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(T&& t); // T is not array template<class T, class A> shared_ptr<T> allocate_shared(const A& a, T&& t); // T is not arrayReturns: Ashared_ptr
to an object of typeT
, initialized tomove(t)
.Remarks: These overloads shall only participate in overload resolution whenT
is not an array type.[ Example:shared_ptr<pair<string,int>> p = make_shared<pair<string,int>>({"test",2}); // shared_ptr to pair{"test",2}shared_ptr<vector<int>> q = make_shared<vector<int>>({16,1}); // shared_ptr to vector with contents {16,1}— end example ].
template<class T, class... Args> shared_ptr<T> make_shared(size_t N, Args&&... args); // T is U[] template<class T, class A, class... Args> shared_ptr<T> allocate_shared(const A& a, size_t N, Args&&... args); // T is U[]Returns: Ashared_ptr
to an object of typeU[N]
, where each innermost array element of typeE == remove_all_extents<T>::type
is initialized toE(forward<Args>(args)...)
.Remarks: These overloads shall only participate in overload resolution whenT
is of the formU[]
.[ Example:shared_ptr<double[]> p = make_shared<double[]>(1024, 1.0); // shared_ptr to a double[1024], where each double is 1.0shared_ptr<double[][2][2]> q = make_shared<double[][2][2]>(6, 1.0); // shared_ptr to a double[6][2][2], where each double is 1.0— end example ].
template<class T, class... Args> shared_ptr<T> make_shared(Args&&... args); // T is U[N] template<class T, class A, class... Args> shared_ptr<T> allocate_shared(const A& a, Args&&... args); // T is U[N]Returns: Ashared_ptr
to an object of typeT
, where each innermost array element of typeE == remove_all_extents<T>::type
is initialized toE(forward<Args>(args)...)
.Remarks: These overloads shall only participate in overload resolution whenT
is of the formU[N]
.[ Example:shared_ptr<double[1024]> p = make_shared<double[1024]>(1.0); // shared_ptr to a double[1024], where each double is 1.0shared_ptr<double[6][2][2]> q = make_shared<double[6][2][2]>(1.0); // shared_ptr to a double[6][2][2], where each double is 1.0— end example ].
template<class T> shared_ptr<T> make_shared(size_t N, typename remove_all_extents<T>::type&& e); // T is U[] template<class T, class A> shared_ptr<T> allocate_shared(const A& a, size_t N, typename remove_all_extents<T>::type&& e); // T is U[]Returns: Ashared_ptr
to an object of typeU[N]
, where each innermost array element of typeE == remove_all_extents<T>::type
is initialized toe
.Remarks: These overloads shall only participate in overload resolution whenT
is of the formU[]
.[ Example:shared_ptr<vector<int>[]> p = 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(typename remove_all_extents<T>::type&& e); // T is U[N] template<class T, class A> shared_ptr<T> allocate_shared(const A& a, typename remove_all_extents<T>::type&& e); // T is U[N]Returns: Ashared_ptr
to an object of typeU[N]
, where each innermost array element of typeE == remove_all_extents<T>::type
is initialized toe
.Remarks: These overloads shall only participate in overload resolution whenT
is of the formU[N]
.[ Example:shared_ptr<vector<int>[4]> p = 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(initializer_list<U> list); // T is U[] template<class T, class A> shared_ptr<T> allocate_shared(const A& a, initializer_list<U> list); // T is U[]Returns: Ashared_ptr
to an object of typeU[N]
, where each array element of typeU
is initialized to the corresponding element fromlist
.Remarks: These overloads shall only participate in overload resolution whenT
is of the formU[]
.[ Example:shared_ptr<int[]> p = make_shared<int[]>({1,2,3,4,5,6}); // shared_ptr to an int[6] with contents {1,2,3,4,5,6}— end example ].
template<class T> shared_ptr<T> make_shared(const T& list); // T is U[N] template<class T, class A> shared_ptr<T> allocate_shared(const A& a, const T& list); // T is U[N]Returns: Ashared_ptr
to an object of typeT
initialized fromlist
.Remarks: These overloads shall only participate in overload resolution whenT
is of the formU[N]
.[ Example:shared_ptr<int[6]> p = make_shared<int[6]>({1,2,3,4,5,6}); // shared_ptr to an int[6] with contents {1,2,3,4,5,6}— end example ].
template<class T> shared_ptr<T> make_shared(size_t N, const U (&list)[M]); // T is U[][M] template<class T, class A> shared_ptr<T> allocate_shared(const A& a, size_t N, const U (&list)[M]); // T is U[][M]Returns: Ashared_ptr
to an object of typeU[N][M]
, where each array element of typeU[M]
is initialized tolist
.Remarks: These overloads shall only participate in overload resolution whenT
is of the formU[]
.[ Example:shared_ptr<int[][2]> p = make_shared<int[][2]>(16,{1,2}); // shared_ptr to an int[16][2], each int[2] subarray having contents {1,2}— end example ].
template<class T> shared_ptr<T> make_shared(const U (&list)[M]); // T is U[N][M] template<class T, class A> shared_ptr<T> allocate_shared(const A& a, const U (&list)[M]); // T is U[N][M]Returns: Ashared_ptr
to an object of typeU[N][M]
, where each array element of typeU[M]
is initialized tolist
.Remarks: These overloads shall only participate in overload resolution whenT
is of the formU[]
.[ Example:shared_ptr<int[16][2]> p = make_shared<int[16][2]>({1,2}); // shared_ptr to an int[16][2], each int[2] subarray having contents {1,2}— end example ].
template<class T> shared_ptr<T> make_shared_noinit(); // T is not array template<class T, class A> shared_ptr<T> allocate_shared_noinit(const A& a); // T is not arrayReturns: Ashared_ptr
to a default-initialized object of typeT
.Remarks: These overloads shall only participate in overload resolution whenT
is not an array type.[ 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— 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: Ashared_ptr
to a default-initialized object of typeU[N]
.Remarks: These overloads shall only participate in overload resolution whenT
is of the formU[]
.[ 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 ].
template<class T> shared_ptr<T> make_shared_noinit(); // T is U[N] template<class T, class A> shared_ptr<T> allocate_shared_noinit(const A& a); // T is U[N]Returns: Ashared_ptr
to a default-initialized object of typeT
.Remarks: These overloads shall only participate in overload resolution whenT
is of the formU[N]
.[ Example:shared_ptr<double[1024]> p = make_shared_noinit<double[1024]>(); // shared_ptr to a default-initialized double[1024], with the elements left unitialized— end example ].
Thanks to Glen Fernandes, who implemented boost::make_shared
for arrays.
— end