Document Number: N3920
Programming Language C++
Library Working Group
 
Peter Dimov, <pdimov@pdimov.com>
 
2014-02-13

Extending shared_ptr to Support Arrays, Revision 2

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

Changes in Revision 2

Incorporated wording suggestions from LEWG and LWG.

Changes in Revision 1

Incorporated wording suggestions from Daniel Krügler.

Motivation

Currently, if an apropriate deleter is used, std::shared_ptr can hold a pointer to an array allocated with new[], but it doesn't offer direct support for that in the interface. This is an obvious omission, an inconsistency with std::unique_ptr, and even the subject of a national body comment (US 105). This paper suggests we take the steps to rectify this unfortunate situation, by making shared_ptr<T[]> and shared_ptr<T[N]> work as expected.

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.

boost::make_shared has also been extended to support arrays. The changes herein are a necessary prerequisite for the make_shared extensions, which are a subject of a separate paper (N3870).

Complexity

Due to the way shared_ptr and weak_ptr are specified and implemented, they already contain most of the functionality that is required, so the changes are relatively minor and local in both cases. The underlying semantics are already present, as shared_ptr can already hold pointers to arrays via an appropriate deleter. In addition, its behavior does not generally depend on the template parameter T, which is why most of the code and wording does not mind dealing with T = U[] or U[N].

It should also be noted that shared_ptr<Y> is convertible to shared_ptr<T> when Y* is convertible to T* and that it so happens that the core language cooperates very well with us in this case by making pointers to array types convertible only via qualification conversions, which is exactly what we want.

The unfortunate occurence of unique_ptr having lost its support for U[N] obliges me to assert that in shared_ptr case, said support is essentially free, both in terms of implementation complexity or specification complexity. Please don't remove it. Consider reinstating unique_ptr<U[N]> instead.

reinterpret_pointer_cast

The proposed text suggests the addition of reinterpret_pointer_cast, to match the other casts and complete the family. It's useful when dealing with arrays of layout-compatible types, but is not essential for the proposal. It could be sacrificed if necessary.

Proposed Text

(All edits are relative to N3485.)

Add the following to the <memory> synopsis in 20.6.2 [memory.syn]:

// 20.7.2.2.9, shared_ptr casts:
template<class T, class U>
shared_ptr<T> static_pointer_cast(shared_ptr<U> const& r) noexcept;
template<class T, class U>
shared_ptr<T> dynamic_pointer_cast(shared_ptr<U> const& r) noexcept;
template<class T, class U>
shared_ptr<T> const_pointer_cast(shared_ptr<U> const& r) noexcept;
template<class T, class U>
shared_ptr<T> reinterpret_pointer_cast(shared_ptr<U> const& r) noexcept;

Make the following change to 20.7.2.2 [util.smartptr.shared]:

public:
  typedef T typename remove_extent<T>::type element_type;

Make the following change to 20.7.2.2 [util.smartptr.shared]:

template<class Y> shared_ptr(const shared_ptr<Y>& r, T element_type *p) noexcept;

Make the following change to 20.7.2.2 [util.smartptr.shared]:

// 20.7.2.2.5, observers:
T* element_type* get() const noexcept;
T& operator*() const noexcept;
T* operator->() const noexcept;
element_type& operator[](ptrdiff_t i) const noexcept;

Make the following change to 20.7.2.2 [util.smartptr.shared] p1:

// 20.7.2.2.9, shared_ptr casts:
template<class T, class U>
shared_ptr<T> static_pointer_cast(const shared_ptr<U>& r) noexcept;
template<class T, class U>
shared_ptr<T> dynamic_pointer_cast(const shared_ptr<U>& r) noexcept;
template<class T, class U>
shared_ptr<T> const_pointer_cast(const shared_ptr<U>& r) noexcept;
template<class T, class U>
shared_ptr<T> reinterpret_pointer_cast(const shared_ptr<U>& r) noexcept;

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

Specializations of shared_ptr shall be CopyConstructible, CopyAssignable, and LessThanComparable, allowing their use in standard containers. Specializations of shared_ptr shall be contextually convertible to bool, allowing their use in boolean expressions and declarations in conditions. The template parameter T of shared_ptr may be an incomplete type.

Add a new paragraph to 20.7.2.2 [util.smartptr.shared] after p4:

For the purposes of subclause 20.7.2 [util.smartptr], a pointer type Y* is said to be compatible with a pointer type T* when either Y* is convertible to T* or Y is U[N] and T is U cv [].

Change 20.7.2.2.1 [util.smartptr.shared.const] p3, p4, p7 as follows:

template<class Y> explicit shared_ptr(Y* p);
Requires: p shall be convertible to T*. Y shall be a complete type. The expression delete p shall be well formed, shall have well defined behavior, and shall not throw exceptions. Y shall be a complete type. The expression delete[] p, when T is an array type, or delete p, when T is not an array type, shall be well-formed, shall have well defined behavior, and shall not throw exceptions. When T is U[N], Y(*)[N] shall be convertible to T*; when T is U[], Y(*)[] shall be convertible to T*; otherwise, Y* shall be convertible to T*.
Effects: When T is not an array type, cConstructs a shared_ptr object that owns the pointer p. Otherwise, constructs a shared_ptr that owns p and a deleter of an unspecified type that calls delete[] p.
Posconditions: use_count() == 1 && get() == p.
Throws: bad_alloc, or an implementation-defined exception when a resource other than memory could not be obtained.
Exception safety: If an exception is thrown, delete p is called when T is not an array type, delete[] p otherwise.

Change 20.7.2.2.1 [util.smartptr.shared.const] p8 as follows:

template<class Y, class D> shared_ptr(Y* p, D d);
template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
template <class D> shared_ptr(nullptr_t p, D d);
template <class D, class A> shared_ptr(nullptr_t p, D d, A a);
Requires: p shall be convertible to T*. D shall be CopyConstructible. The copy constructor and destructor of D shall not throw exceptions. The expression d(p) shall be well formed, shall have well defined behavior, and shall not throw exceptions. A shall be an allocator (17.6.3.5). The copy constructor and destructor of A shall not throw exceptions. When T is U[N], Y(*)[N] shall be convertible to T*; when T is U[], Y(*)[] shall be convertible to T*; otherwise, Y* shall be convertible to T*.

Change the line between 20.7.2.2.1 [util.smartptr.shared.const] p12 and p13 as follows:

template<class Y> shared_ptr(const shared_ptr<Y>& r, T element_type *p) noexcept;

Change 20.7.2.2.1 [util.smartptr.shared.const] p17 as follows:

shared_ptr(const shared_ptr& r) noexcept;
template<class Y> shared_ptr(const shared_ptr<Y>& r) noexcept;
Requires: The second constructor shall not participate in the overload resolution unless Y* is implicitly convertible to compatible with T*.

Change 20.7.2.2.1 [util.smartptr.shared.const] p20 as follows:

shared_ptr(shared_ptr&& r) noexcept;
template<class Y> shared_ptr(shared_ptr<Y>&& r) noexcept;
Requires: The second constructor shall not participate in the overload resolution unless Y* is implicitly convertible to compatible with T*.

Change 20.7.2.2.1 [util.smartptr.shared.const] p23 as follows:

template<class Y> explicit shared_ptr(const weak_ptr<Y>& r);
Requires: Y* shall be convertible to compatible with T*.

Add a new paragraph before 20.7.2.2.1 [util.smartptr.shared.const] p33:

template <class Y, class D> shared_ptr(unique_ptr<Y, D>&&r);
Requires: Y* shall be compatible with T*.
Effects: Equivalent to shared_ptr(r.release(), r.get_deleter()) when D is not a reference type, otherwise shared_ptr(r.release(), ref(r.get_deleter())).

Change 20.7.2.2.5 [util.smartptr.shared.obs] p1-p6 and the preceding line as follows:

T* element_type* get() const noexcept;
Returns: the stored pointer.
T& operator*() const noexcept;
Requires: get() != 0.
Returns: *get().
Remarks: When T is an array type or cv-qualified void, it is unspecified whether this member function is declared. If it is declared, it is unspecified what its return type is, except that the declaration (although not necessarily the definition) of the function shall be well formed.
T* operator->() const noexcept;
Requires: get() != 0.
Returns: get().
Remarks: When T is an array type, it is unspecified whether this member function is declared. If it is declared, it is unspecified what its return type is, except that the declaration (although not necessarily the definition) of the function shall be well formed.
element_type& operator[](ptrdiff_t i) const noexcept;
Requires: get() != 0 && i >= 0. If T is U[N], i < N.
Returns: get()[i].
Remarks: When T is not an array type, it is unspecified whether this member function is declared. If it is declared, it is unspecified what its return type is, except that the declaration (although not necessarily the definition) of the function shall be well formed.

Change 20.7.2.2.9 [util.smartptr.shared.cast] p1, p2, p3 as follows:

template<class T, class U> shared_ptr<T> static_pointer_cast(const shared_ptr<U>& r) noexcept;
Requires: The expression static_cast<T*>(r.get()) static_cast<T*>((U*)0) shall be well formed.
Returns: If r is empty, an empty shared_ptr<T>; otherwise, a shared_ptr<T> object that stores static_cast<T*>(r.get()) and shares ownership with r shared_ptr<T>(r, static_cast<typename shared_ptr<T>::element_type*>(r.get())).
Postconditions: w.get() == static_cast<T*>(r.get()) and w.use_count() == r.use_count(), where w is the return value.

Change 20.7.2.2.9 [util.smartptr.shared.cast] p5, p6, p7 as follows:

template<class T, class U> shared_ptr<T> dynamic_pointer_cast(const shared_ptr<U>& r) noexcept;
Requires: The expression dynamic_cast<T*>(r.get()) dynamic_cast<T*>((U*)0) shall be well formed and shall have well defined behavior.
Effects:
Postconditions: w.get() == dynamic_cast<T*>(r.get()), where w is the return value.

Change 20.7.2.2.9 [util.smartptr.shared.cast] p9, p10, p11 as follows:

template<class T, class U> shared_ptr<T> const_pointer_cast(const shared_ptr<U>& r) noexcept;
Requires: The expression const_cast<T*>(r.get()) const_cast<T*>((U*)0) shall be well formed.
Returns: If r is empty, an empty shared_ptr<T>; otherwise, a shared_ptr<T> object that stores const_cast<T*>(r.get()) and shares ownership with r shared_ptr<T>(r, const_cast<typename shared_ptr<T>::element_type*>(r.get())).
Postconditions: w.get() == const_cast<T*>(r.get()) and w.use_count() == r.use_count(), where w is the return value.

Add the following after 20.7.2.2.9 [util.smartptr.shared.cast] p12:

template<class T, class U> shared_ptr<T> reinterpret_pointer_cast(const shared_ptr<U>& r) noexcept;
Requires: The expression reinterpret_cast<T*>((U*)0) shall be well formed.
Returns: shared_ptr<T>(r, reinterpret_cast<typename shared_ptr<T>::element_type*>(r.get())).

Make the following change to 20.7.2.3 [util.smartptr.weak] p1:

public:
  typedef T typename remove_extent<T>::type element_type;

Change 20.7.2.3.1 [util.smartptr.weak.const] p3 as follows:

weak_ptr(const weak_ptr& r) noexcept;
template<class Y> weak_ptr(const weak_ptr<Y>& r) noexcept;
template<class Y> weak_ptr(const shared_ptr<Y>& r) noexcept;
Requires: The second and third constructors shall not participate in the overload resolution unless Y* is implicitly convertible to compatible with T*.

Thanks to Glen Fernandes, whose contribution of boost::make_shared for arrays prompted the boost::shared_ptr additions proposed herein.

Thanks to Daniel Krügler for reviewing an earlier revision of this paper and suggesting wording improvements.

Thanks to Jonathan Wakely for providing valuable feedback.

— end