Document number: N3073=10-0063
Project: Programming Language C++, Library Working Group
Authors: Daniel Krügler, Walter E. Brown
Date: 2010-03-12

Specifying Pointer-Like Requirements (Revision 1)

Discussion

This paper attempts to resolve several issues relating to unique_ptr and pointer-like species. The approach suggested in this paper is to introduce a new requirement set NullablePointer, and to apply it where applicable within the standard library.

For more details regarding the rationale, see N3025.

Revision History

N3073 - Revision 1

N3025 - Initial version

Issues Resolved

If accepted, this proposal would resolve the following library issues:

Number Description
834 Unique_ptr::pointer requirements underspecified
1293 unique_ptr<T[], D> needs to get rid of unspecified-pointer-type
1307 exception_ptr and allocator pointers don't understand !=
1135 exception_ptr should support contextual conversion to bool
932 unique_ptr(pointer p) for pointer deleter types
950 unique_ptr converting ctor shouldn't accept array form
983 unique_ptr reference deleters should not be moved from
1100 auto_ptr to unique_ptr conversion

Proposed resolution

  1. Change [propagation] as indicated: [The intent is to resolve issue 1307, because exception_ptr does not define the meaning of !=. Further-on an alternative solution of 1135 is provided which delegates contextual conversion to bool to the newly suggested NullablePointer requirements. Note that we cannot strike p. 4 completely, because the more general NullablePointer requirements also support default construction that may produce an indeterminate value in the general case (e.g. built-in pointers or pointers-to-member), but exception_ptr is a class type that is intended to produce a null value even for this kind of creation.]

    1 The type exception_ptr can be used to refer to an exception object.

    2 exception_ptr shall be DefaultConstructible, CopyConstructible, CopyAssignable, and EqualityComparable. exception_ptr's operations shall not throw exceptions satisfy the NullablePointer requirements ([nullablepointer.requirements]).

    3 Two objectsnon-null values of type exception_ptr are equivalent and compare equal if and only if they refer to the same exception.

    4 The default constructor of exception_ptr produces the null value of the type. The null value is equivalent only to itself.

    5 An object of type exception_ptr can be compared for equality with a null pointer constant and assigned a null pointer constant. The effect shall be as if exception_ptr() had been used in place of the null pointer constant.exception_ptr shall not be implicitly convertible to any arithmetic, enumeration, or pointer type.

    6 [ Note: An implementation might use a reference-counted smart pointer as exception_ptr. — end note ]

  2. Add before the current [allocator.requirements] subclause the following new subclause of [utility.requirements]: [ Editor's note: The symbol ?? is used as representative of the expected table number]

    20.2.2 NullablePointer requirements [nullablepointer.requirements]

    A NullablePointer type is a pointer-like type that supports null values. A type P meets the NullablePointer requirements if:

    A value-initialized object of P produces the null value of the type. The null value shall be equivalent only to itself. A default-initialized object of P may have an indeterminate value. [Note: Operations involving indeterminate values may trigger undefined behavior. — end note]

    An object p of P can be contextually converted to bool ([conv]). The effect shall be as if p != nullptr had been evaluated in place of p.

    No operation which is part of the NullablePointer requirements shall exit via an exception.

    In Table ??, u denotes an identifier, t denotes a non-const lvalue of type P, a and b denote values of type (possibly const) P, and np denotes a value of type (possibly const) std::nullptr_t.

    Table ?? — Additional NullablePointer requirements
    Expression Return type Operational semantics
    P u(np);
    P u = np;
    post: u == nullptr
    P(np) post: P(np) == nullptr
    t = np P& post: t == nullptr
    a != b contextually convertible to bool !(a == b)
    a == np
    np == a
    contextually convertible to bool a == P()
    a != np
    np != a
    contextually convertible to bool !(a == np)
  3. Change [unique.ptr]/1+2 as indicated: [This provides more specific descriptions of the general requirements for all instantiations of unique_ptr]

    1 Template unique_ptr A unique pointer provides strict ownership semantics, owning the object to which it holds a pointer. More precisely, a unique pointer is an object u that stores a pointer to an second object p and thatdeletes that object will dispose of p using the associated deleter when itu is itself destroyed (such as when leaving block scope (6.7)). In this context, u is said to own p.

    2 The mechanism by which u disposes of p is known as p's associated deleter, a function object whose correct invocation results in p's appropriate disposition (typically, its deletion).

    3 Let the notation u.p denote the pointer stored by u, and let u.d denote the associated deleter. Upon request, u can reset (replace) u.p and u.d with another pointer and deleter, but must properly dispose of its owned object via the associated deleter before such replacement is considered completed.

    4 Additionally, u can upon request transfer ownership to another unique pointer u2. Upon completion of such a transfer, the following postconditions hold:

    As in the case of a reset, u2 must properly dispose of its pre-transfer owned object via the pre-transfer associated deleter before the ownership transfer is considered completed. [Note: A deleter's state need never be copied, only moved or swapped as ownership is transferred. &mdash end note ]

    5 The unique_ptr provides a semantics of strict ownership. A unique_ptr owns the object it holds a pointer to. Each object of a type U instantiated from the unique_ptr template specified in this subclause has the strict ownership semantics, specified above, of a unique pointer. In partial achievement of these semantics, each such UA unique_ptr is notneither CopyConstructible, nor CopyAssignable, however it is MoveConstructible and MoveAssignable. The template parameter T of unique_ptr may be an incomplete type.

    6 [Note: The uses of unique_ptr include providing exception safety for dynamically allocated memory, passing ownership of dynamically allocated memory to a function, and returning dynamically allocated memory from a function. — end note]

  4. Add to [unique.ptr.single], class unique_ptr synopsis the following declarations: [This solves 1100, but the wording needs some tweaking to take care of more general pointer types]

    template <class T, class D = default_delete<T>> class unique_ptr {
    public:
        ...
        // constructors
        ...
        template <class U, class E> unique_ptr(unique_ptr<U, E>&& u);
        template <class U> explicit unique_ptr(auto_ptr<U>& u);
        template <class U> unique_ptr(auto_ptr<U>&& u);
        ...
    };
    
  5. Change [unique.ptr.single] as indicated: [The intent is to replace the coupling between T* and the deleter's operator() by a coupling between unique_ptr<T, D>::pointer and the deleter's operator(), see 834. Another fix is related to requirements of the deleter: It is unclear here, whether they are purely descriptive or normative. Since we specify the requirements on a per-member base, we can strike this sentence here, but add the missing Destructible requirement.] [Editor's note: p. 1 provides a resolution that is considered superior to that of 870, bullet 14 and should be applied instead of that.]

    1 The default type for the template parameter D is default_delete. A client-supplied template argument for D shall be a function pointer or functorobject type ([function.objects]), lvalue-reference to function, or lvalue-reference to function object type for which, given a value d of type D and a pointervalue ptr of type T*unique_ptr<T, D>::pointer, the expression d(ptr) is valid and has the effect of deallocating disposing of the pointer as appropriate for that deleter. D may also be an lvalue-reference to a deleter.

    2 If the deleter D maintains state, it is intended that this state stay with the associated pointer as ownership is transferred from unique_ptr to unique_ptr. The deleter state need never be copied, only moved or swapped as pointer ownership is moved around. That is, the deleter need only be MoveConstructible, MoveAssignable, and Swappable, and need not be CopyConstructible (unless copied into the unique_ptr) nor CopyAssignable. If the deleter's type D is not a reference type, D shall satisfy the Destructible requirements ([destructible]).

    3 If the type remove_reference<D>::type::pointer exists, then unique_ptr<T, D>::pointer shall be a synonym for remove_reference<D>::type::pointer. Otherwise unique_ptr<T, D>::pointer shall be a synonym for T*. The type unique_ptr<T, D>::pointer shall be CopyConstructible (Table 34) and CopyAssignable (Table 36)satisfy the NullablePointer requirements ([nullablepointer.requirements]).

    4 [Example: Given an allocator type X ([allocator.requirements]) and letting the type A be a synonym for allocator_traits<X>, the types A::pointer, A::const_pointer, A::void_pointer, and A::const_void_pointer may be used as unique_ptr<T, D>::pointer. — end example]

  6. Change [unique.ptr.single.ctor] as indicated: [The changes ensure that we now explicitly say, how the stored pointer and stored deleter are initialized, which is especially important for a constexpr function. Additionally we fix a language lapse here, because the term "default constructible" is not defined. (834). Note that we solve 932 as well]

    constexpr unique_ptr();

    1 Requires: D shall be default constructiblesatisfy the DefaultConstructible requirements, and that construction shall not throw an exception. D shall not be a reference type or pointer type (diagnostic required).

    2 Effects: Constructs a unique_ptr which owns nothing, value-initializing the stored pointer and the stored deleter.

    3 Postconditions:

    • get() == 0nullptr.
    • get_deleter() returns a reference to a value-initializedthe stored deleter D.

    4 Throws: nothing.

    5 Remarks: If this constructor is instantiated with a pointer type or reference type argument for the template parameter D, the program is ill-formed.

  7. Change [unique.ptr.single.ctor] as indicated: [This is a step-by-fix to ensure consistency to the changes of N2976. Additionally we fix a language lapse here, because the term "default constructible" is not defined. (834). Note that we solve 932 as well]

    unique_ptr(pointer p);

    5 Requires: D shall be default constructiblesatisfy the DefaultConstructible requirements, and that construction shall not throw an exception.

    6 Effects: Constructs a unique_ptr which owns p, initializing the stored pointer with p and value-initializing the stored deleter.

    7 Postconditions:

    • get() == p.
    • get_deleter() returns a reference to a default constructedthe stored deleter D.

    8 Throws: nothing.

    9 Remarks: If this constructor is instantiated with a pointer type or reference type for the template parameter D, the program is ill-formed.

  8. Change [unique.ptr.single.ctor] as indicated: [The intent is to fix the current lack of specification in which way the stored pointer is initialized (834) and to fix some wording impreciseness regarding deleter requirements.]

    unique_ptr(pointer p, implementation-definedsee below d1);
    unique_ptr(pointer p, implementation-definedsee below d2);

    ...

    12 Requires: If D is not an lvalue-reference type then

    • If d is an lvalue or const rvalue then the first constructor of this pair will be selected. D must be shall satisfy the CopyConstructible requirements (Table 34), and this unique_ptr will hold a copy of d. The copy constructor of D shall not throw an exception.
    • Otherwise d is a non-const rvalue and the second constructor of this pair will be selected. D need only beshall satisfy the MoveConstructible requirements (Table 33), and this unique_ptr will hold a value move constructed from d. The move constructor of D shall not throw an exception.

    ...

    Effects: Constructs a unique_ptr which owns p, initializing the stored pointer with p and initializing the deleter as described above.

    14 Postconditions:

    • get() == p.
    • get_deleter() returns a reference to the internally stored deleter.
    • If D is a reference type then get_deleter() returns a reference to the lvalue d.
  9. Change [unique.ptr.single.ctor] as indicated: [The intent is to clarify that the moved-from source must contain a null pointer, there is no other choice left and to fix some wording impreciseness regarding deleter requirements. With the possibility of user-defined pointer-like types the implication does only exist, if those are built-in pointers. (834). Note that we solve 950 modulo type-completeness constraints and 983 as well]

    unique_ptr(unique_ptr&& u);

    16 Requires: If the deleter D is not a reference type, D shall satisfy the MoveConstructible requirements. cConstruction of the deleter D from an rvalue of type D shall not throw an exception.

    17 Effects: Constructs a unique_ptr by transferring ownership from u to *thiswhich owns the pointer which u owns (if any). If the deleterD is not a reference type, itthis deleter is move constructed from u's deleter, otherwise the referencethis deleter is copy constructed from u's deleter. After the construction, u no longer owns a pointer. [Note: The deleter constructor can be implemented with std::forward<D>. — end note]

    18 Postconditions:

    • get() == yields the value u.get() hadyielded before the construction. get_deleter() returns a reference to the internally stored deleter whichthat was constructed from u.get_deleter().
    • If D is a reference type then get_deleter() and u.get_deleter() both reference the same lvalue deleter.

    19 - Throws: nothing.

    template <class U, class E> unique_ptr(unique_ptr<U, E>&& u);

    20 - Requires: If DE is not a reference type, construction of the deleter D from an rvalue of type E shall be well formed and shall not throw an exception. Otherwise E is a reference type and construction of the deleter from an lvalue of type E shall be well formed and shall not throw an exception.If D is a reference type, then E shall be the same type as D (diagnostic required). unique_ptr<U, E>::pointer shall be implicitly convertible to pointer. [Note: These requirements imply that T and U are complete types. — end note]

    ? - Remarks: This constructor shall not participate in overload resolution unless:

    • unique_ptr<U, E>::pointer is implicitly convertible to pointer, and
    • U is not an array type, and
    • D is a reference type and E is the same type as D, or E is implicitly convertible to D.

    21 Effects: Constructs a unique_ptr by transferring ownership from u to *thiswhich owns the pointer which u owns (if any). If the deleter E is not a reference type, itthis deleter is move constructed from u's deleter, otherwise the referencethis deleter is copy constructed from u's deleter. After the construction, u no longer owns a pointer. [Note: The deleter constructor can be implemented with std::forward<DE>. — end note]

    22 Postconditions:

    • get() == yields the value u.get() had yielded before the construction, modulo any required offset adjustments resulting from the cast from unique_ptr<U, E>::pointer to pointer.
    • get_deleter() returns a reference to the internally stored deleter whichthat was constructed from u.get_deleter().

    23 Throws: nothing.

  10. At the end of [unique.ptr.single.ctor] add the following sequence of paragraphs. [This solves 1100, additionally honoring the more general pointer concept and specifies some missing effects and post-condition details.]

    template <class U> explicit unique_ptr(auto_ptr<U>& u);
    template <class U> unique_ptr(auto_ptr<U>&& u);
    
    Effects: Constructs a unique_ptr, initializing the stored pointer with u.release() and value-initializing the stored deleter.

    Postconditions:

    • get() yields the value u.get() yielded before the construction.
    • u.get() == nullptr.
    • get_deleter() returns a reference to the stored deleter.

    Throws: nothing.

    Remarks: These constructors shall not participate in overload resolution unless U* is implicitly convertible to pointer and D is the same type as default_delete<T>.

  11. Change [unique.ptr.single.dtor]/2 as indicated (834):

    ~unique_ptr();

    1 Requires: The expression get_deleter()(get()) shall be well formed, shall have well-defined behavior, and shall not throw exceptions. [Note: The use of default_delete requires T to be a complete type. — end note]

    2 Effects: If get() == 0nullptr there are no effects. Otherwise get_deleter()(get()).

    3 Throws: nothing.

  12. Change [unique.ptr.single.asgn] as indicated: [The intent is to clarify that the moved-from source must contain a null pointer, there is no other choice left. With the possibility of user-defined pointer-like types the implication does only exist, if those are built-in pointers (834). Note that we solve 950 modulo type-completeness constraints and 983 as well]:

    unique_ptr& operator=(unique_ptr&& u);

    1 Requires: If D is not a reference type, D shall satisfy the MoveAssignable requirements and aAssignment of the deleter D from an rvalue of type D shall not throw an exception. Otherwise D is a reference type, remove_reference<D>::type shall satisfy the CopyAssignable requirements, and assignment of the deleter from an lvalue of type D shall not throw an exception.

    2 Effects: Transfers ownership from u to *this as if by calling reset(u.release()) followed by an move assignment from u's deleter to this deleterstd::forward<D>(u.get_deleter()).

    3 Postconditions: This unique_ptr now owns the pointer which u owned, and u no longer owns it. [Note: If D is a reference type, then the referenced lvalue deleters are move assigned. — end note]

    4 Returns: *this.

    5 Throws: nothing.

    template <class U, class E> unique_ptr& operator=(unique_ptr<U, E>&& u);

    6 Requires: If E is not a reference type, aAssignment of the deleter D from an rvalue of type ED shall be well-formed and shall not throw an exception. Otherwise the deleter E is a reference type, and assignment of the deleter D from an lvalue of type E shall be well-formed and shall not throw an exception. unique_ptr<U, E>::pointer shall be implicitly convertible to pointer. [Note: These requirements imply that T and U are complete types. — end note]

    7 Remarks: This operator shall not participate in overload resolution unless:

    • unique_ptr<U, E>::pointer is implicitly convertible to pointer, and
    • U is not an array type.

    8 Effects: Transfers ownership from u to *this as if by calling reset(u.release()) followed by an move assignment from u's deleter to this deleterstd::forward<D>(u.get_deleter()). If either D or E is a reference type, then the referenced lvalue deleter participates in the move assignment.

    9 - Postconditions: This unique_ptr now owns the pointer which u owned, and u no longer owns it.

  13. Change [unique.ptr.single.asgn] before p. 11 and p. 12 as indicated: [The first change is a simple typo fix (834)]:

    unique_ptr& operator=(nullptr_t});

    11 Effects: reset().

    12 Postcondition: get() == 0nullptr

    13 Returns: *this.

    14 Throws: nothing.

  14. Change [unique.ptr.single.observers] as indicated (834) [For details see the Remarks section]:

    typename add_lvalue_reference<T>::type operator*() const;

    1 Requires: get() != 0nullptr.

    2 Returns: *get().

    3 Throws: nothing.

    pointer operator->() const;

    4 Requires: get() != 0nullptr.

    5 Returns: get().

    6 Throws: nothing.

    7 Note: use typically requires that T be a complete type.

    explicit operator bool() const;

    12 Returns: get() != 0nullptr.

    13 Throws: nothing.

  15. Change [unique.ptr.single.modifiers]/1 as indicated (834):

    pointer release();

    1 Postcondition: get() == 0nullptr.

    2 Returns: The value get() had at the start of the call to release.

    3 Throws: nothing.

  16. Change [unique.ptr.single.modifiers] as indicated: [The intent is to ensure that potentially user-defined swaps are used. The used specification is as fuzzy in regard to the meaning of the exact swap call as other parts of the standard. It is intended to make this clearer as a result of the solution of issue 524. A side-step fix and harmonization with the specification of the deleter is realized. We don't need to add the Swappable requirement for pointer, because those are already part of the NullablePointer requirements, including no-throw requirements] (834):

    void swap(unique_ptr& u);

    8 Requires: The deleter Dget_deleter() shall be Swappableswappable ([swappable.requirements]), and shall not throw an exception under swap.

    9 Effects: The stored pointers of this and u are exchanged. The stored deleters are swap'd (unqualified)Invokes swap on the stored pointers and on the stored deleters of *this and u.

  17. Change [unique.ptr.runtime], class template unique_ptr<T[], D> synopsis as indicated (1293)

    // assignment
    unique_ptr& operator=(unique_ptr&& u);
    unique_ptr& operator=(unspecified-pointer-typenullptr_t);

  18. Change [unique.ptr.runtime.observers] as indicated [For details see the Remarks section] (834):

    T& operator[](size_t i) const;

    1 Requires: i < the size of the array to which the stored pointer points.

    2 Returns: get()[i].

    3 Throws: nothing.

  19. Change [unique.ptr.runtime.modifiers]/1 as indicated (834):

    void reset(pointer p = pointer());
    void reset(nullptr_t p);

    1 Effects: If get() == 0nullptr there are no effects. Otherwise get_deleter()(get()).

  20. Let [unique.ptr.special] remain unchanged: [Note that the relational operators are not part of the basic requirement set, and that we want to support mixed ==, which is beyond the EqualityComparable requirements. For details see the Remarks section.] (834):

    template <class T1, class D1, class T2, class D2>
    bool operator==(const unique_ptr<T1, D1>& x, const unique_ptr<T2, D2>& y);

    2 Returns: x.get() == y.get().

    template <class T1, class D1, class T2, class D2>
    bool operator!=(const unique_ptr<T1, D1>& x, const unique_ptr<T2, D2>& y);

    3 Returns: x.get() != y.get().

    template <class T1, class D1, class T2, class D2>
    bool operator<(const unique_ptr<T1, D1>& x, const unique_ptr<T2, D2>& y);

    4 Returns: x.get() < y.get().

    template <class T1, class D1, class T2, class D2>
    bool operator<=(const unique_ptr<T1, D1>& x, const unique_ptr<T2, D2>& y);

    5 Returns: x.get() <= y.get().

    template <class T1, class D1, class T2, class D2>
    bool operator>(const unique_ptr<T1, D1>& x, const unique_ptr<T2, D2>& y);

    6 Returns: x.get() > y.get().

    template <class T1, class D1, class T2, class D2>
    bool operator>=(const unique_ptr<T1, D1>& x, const unique_ptr<T2, D2>& y);

    7 Returns: x.get() >= y.get().

  21. Change Table 40 — Allocator requirements as indicated: [All removed entries are already specified via the NullablePointer requirements]

    Table 40 — Allocator requirements
    Expression Return type Assertion/note
    pre-/post-condition
    Default
    ... ... ... ...
    pointer d(nullptr)
    pointer d = nullptr
    const_pointer e(nullptr)
    const_pointer e = nullptr
    d and e are null pointers and need not be dereferenceable.
    static_cast<bool>(d) == false,
    static_cast<bool>(e) == false
    void_pointer d(nullptr)
    void_pointer d = nullptr
    const_void_pointer e(nullptr)
    const_void_pointer e = nullptr
    d and e are null pointers and need not be dereferenceable.
    static_cast<bool>(d) == false,
    static_cast<bool>(e) == false
    p contextually convertible to bool false only if p is a null pointer
    q contextually convertible to bool false only if q is a null pointer
    w contextually convertible to bool false only if w is a null pointer
    z contextually convertible to bool false only if z is a null pointer
    !p contextually convertible to bool true only if p is a null pointer
    !q contextually convertible to bool true only if q is a null pointer
    !w contextually convertible to bool true only if w is a null pointer
    !z contextually convertible to bool true only if z is a null pointer
    ... ... ... ...
  22. Change [allocator.requirements]/4 as indicated [All removed parts are already specified via the NullablePointer requirements. This change additionally ensures that != is now well-defined for the allocator pointer types and fixes therefore issue 1307]:

    The X::pointer, X::const_pointer, X::void_pointer, and X::const_void_pointer types shall satisfy the NullablePointer requirements ([nullablepointer.requirements])of EqualityComparable, DefaultConstructible, CopyConstructible, CopyAssignable, Swappable, and Destructible (20.2.1). No constructor, comparison operator, copy operation, move operation, or swap operation on these types shall exit via an exception.A default-initialized object may have a singular value. A value-initialized object shall compare equal to nullptr. X::pointer and X::const_pointer shall also satisfy the requirements for a random access iterator (24.2).

Remarks

During reviews of this paper it became controversial how to properly specify the operational semantics of operator*, operator[], and the heterogenous comparison functions. [structure.specifications]/3 doesn't clearly say whether a Returns element (in the absence of the new Equivalent to formula) specifies effects. Further-on it's unclear whether this would allow for such a return expression to exit via an exception, if additionally a Throws:-Nothing element is provided (would the implementor be required to catch those?). To resolve this conflict, any existing Throws element was removed for these operations, which is at least consistent with [unique.ptr.special] and other parts of the standard. The result of this is that we give now implicit support for potentially throwing comparison functions, but not for homogeneous == and !=, which might be a bit surprising.

Acknowledgements

The first author would like to express special thanks to Howard Hinnant and Alisdair Meredith for many detailed improvement suggestions and reviews during several stages of this proposal and to Alberto Ganesh Barbati, Beman Dawes, and Peter Dimov for valuable discussions and improvement suggestions. The second author acknowledges the Fermi National Accelerator Laboratory's Computing Division for sponsoring his participation in the C++ standardization effort.