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 ofNvalue-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*Mvalue-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 ofNelements 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*Melements 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 ofNdefault-initialized elements of typeT. Useful whenTis a built-in type such asdouble. Multidimensional arrays are supported. The scalar version ofmake_sharedis 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 ofNelements 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 whenTis an aggregate or a container. The scalar version ofmake_sharedis 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 ofNelements 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, withintreplaced 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_noinitoverloads, 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:Ashall be an allocator (17.6.3.5). The copy constructor and destructor ofAshall not throw exceptions.Effects: Allocates memory for an object of typeT(orU[N]whenTisU[], whereNis determined fromargsas specified by the concrete overload). The object is initialized fromargsas specified by the concrete overload. The templatesallocate_sharedandallocate_shared_noinituse a copy ofato allocate memory. If an exception is thrown, the functions have no effect.Returns: Ashared_ptrinstance that stores and owns the address of the newly constructed object.Postconditions:r.get() != 0 && r.use_count() == 1, whereris 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 typeUis 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 typeUis specified to be initialized to a valuev, or toU(l...), wherel...is a list of constructor arguments,make_sharedshall perform this initialization via the expression::new(pv) U(v)or::new(pv) U(l...)respectively, wherepvhas typevoid*and points to storage suitable to hold an object of typeU.When a (sub)object of typeUis specified to be initialized to a valuev, or toU(l...), wherel...is a list of constructor arguments,allocate_sharedshall perform this initialization via the expressionallocator_traits<A2>::construct(a2, pv, v)orallocator_traits<A2>::construct(a2, pv, l...)respectively, wherepvpoints to storage suitable to hold an object of typeUanda2of typeA2is a rebound copy of the allocatorapassed toallocate_sharedsuch that itsvalue_typeisU.When a (sub)object of typeUis specified to be default-initialized,make_shared_noinitandallocate_shared_noinitshall perform this initialization via the expression::new(pv) U, wherepvhas 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_ptrto an object of typeT, initialized toT(forward<Args>(args)...).Remarks: These overloads shall only participate in overload resolution whenTis 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_ptrto an object of typeT, initialized tomove(t).Remarks: These overloads shall only participate in overload resolution whenTis 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_ptrto an object of typeU[N], where each innermost array element of typeE == remove_all_extents<T>::typeis initialized toE(forward<Args>(args)...).Remarks: These overloads shall only participate in overload resolution whenTis 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_ptrto an object of typeT, where each innermost array element of typeE == remove_all_extents<T>::typeis initialized toE(forward<Args>(args)...).Remarks: These overloads shall only participate in overload resolution whenTis 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_ptrto an object of typeU[N], where each innermost array element of typeE == remove_all_extents<T>::typeis initialized toe.Remarks: These overloads shall only participate in overload resolution whenTis 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_ptrto an object of typeU[N], where each innermost array element of typeE == remove_all_extents<T>::typeis initialized toe.Remarks: These overloads shall only participate in overload resolution whenTis 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_ptrto an object of typeU[N], where each array element of typeUis initialized to the corresponding element fromlist.Remarks: These overloads shall only participate in overload resolution whenTis 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_ptrto an object of typeTinitialized fromlist.Remarks: These overloads shall only participate in overload resolution whenTis 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_ptrto 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 whenTis 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_ptrto 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 whenTis 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_ptrto a default-initialized object of typeT.Remarks: These overloads shall only participate in overload resolution whenTis 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_ptrto a default-initialized object of typeU[N].Remarks: These overloads shall only participate in overload resolution whenTis 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_ptrto a default-initialized object of typeT.Remarks: These overloads shall only participate in overload resolution whenTis 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