Document Number: N3641
Programming Language C++
Library Working Group
 
Peter Dimov, <pdimov@pdimov.com>
 
2013-05-02

Extending make_shared to Support Arrays

This paper proposes adding array support to make_shared, via the syntax make_shared<T[]> and make_shared<T[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.

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).

Syntax

shared_ptr<T[]> p = make_shared<T[]>(N);
shared_ptr<T[N]> q = make_shared<T[N]>();
Creates an array of N value-initialized elements of type T.
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 of N*M value-initialized elements of type T. 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 of N elements of type T, each initialized to T(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 of N*M elements of type T, each initialized to T(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 of N default-initialized elements of type T. Useful when T is a built-in type such as double. Multidimensional arrays are supported. The scalar version of make_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 of N elements of type T, each initialized to {a, b}. Can be used whenever T{a, b} is valid, but is particularly useful when T(a, b) and T{a, b} mean different things, such as when T is an aggregate or a container. The scalar version of make_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 of N elements of type int[3], each initialized to {1, 2, 3}. Conceptually the same as the previous example, with T = 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 type int[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, with int replaced with int[3].

Rationale

Per-element construction with arguments

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.

Explicit support for multidimensional arrays

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 with an initializer list

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.

Whole array construction with an initializer list

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.

Array construction with a repeated initializer list

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.

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(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 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 a (sub)object of 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 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 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<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 array
Returns: A shared_ptr to an object of type T, initialized to move(t).
Remarks: These overloads shall only participate in overload resolution when T 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: A shared_ptr to an object of type U[N], where each innermost array element of type E == remove_all_extents<T>::type is initialized to E(forward<Args>(args)...).
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 double is 1.0
shared_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: A shared_ptr to an object of type T, where each innermost array element of type E == remove_all_extents<T>::type is initialized to E(forward<Args>(args)...).
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 double is 1.0
shared_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: A shared_ptr to an object of type U[N], where each innermost array element of type E == remove_all_extents<T>::type is initialized to e.
Remarks: These overloads shall only participate in overload resolution when T is of the form U[].
[ 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: A shared_ptr to an object of type U[N], where each innermost array element of type E == remove_all_extents<T>::type is initialized to e.
Remarks: These overloads shall only participate in overload resolution when T is of the form U[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: A shared_ptr to an object of type U[N], where each array element of type U is initialized to the corresponding element from list.
Remarks: These overloads shall only participate in overload resolution when T is of the form U[].
[ 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: A shared_ptr to an object of type T initialized from list.
Remarks: These overloads shall only participate in overload resolution when T is of the form U[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: A shared_ptr to an object of type U[N][M], where each array element of type U[M] is initialized to list.
Remarks: These overloads shall only participate in overload resolution when T is of the form U[].
[ 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: A shared_ptr to an object of type U[N][M], where each array element of type U[M] is initialized to list.
Remarks: These overloads shall only participate in overload resolution when T is of the form U[].
[ 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 array
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 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: 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 ].
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: A shared_ptr to a default-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_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