834. unique_ptr::pointer requirements underspecified

Section: 20.3.1.3 [unique.ptr.single] Status: Resolved Submitter: Daniel Krügler Opened: 2008-05-14 Last modified: 2016-01-28

Priority: Not Prioritized

View other active issues in [unique.ptr.single].

View all other issues in [unique.ptr.single].

View all issues with Resolved status.

Discussion:

Issue 673 (including recent updates by 821) proposes a useful extension point for unique_ptr by granting support for an optional deleter_type::pointer to act as pointer-like replacement for element_type* (In the following: pointer).

Unfortunately no requirements are specified for the type pointer which has impact on at least two key features of unique_ptr:

  1. Operational fail-safety.
  2. (Well-)Definedness of expressions.

The unique_ptr specification makes great efforts to require that essentially all operations cannot throw and therefore adds proper wording to the affected operations of the deleter as well. If user-provided pointer-emulating types ("smart pointers") will be allowed, either all throw-nothing clauses have to be replaced by weaker "An exception is thrown only if pointer's {op} throws an exception"-clauses or it has to be said explicitly that all used operations of pointer are required not to throw. I understand the main focus of unique_ptr to be as near as possible to the advantages of native pointers which cannot fail and thus strongly favor the second choice. Also, the alternative position would make it much harder to write safe and simple template code for unique_ptr. Additionally, I assume that a general statement need to be given that all of the expressions of pointer used to define semantics are required to be well-formed and well-defined (also as back-end for 762).

[ Sophia Antipolis: ]

Howard: We maybe need a core concept PointerLike, but we don't need the arithmetic (see shared_ptr vs. vector<T>::iterator.

Howard will go through and enumerate the individual requirements wrt. pointer for each member function.

[ 2009-07 Frankfurt: ]

Move to Ready.

[ 2009-10-15 Alisdair pulls from Ready: ]

I hate to pull an issue out of Ready status, but I don't think 834 is fully baked yet.

For reference the proposed resolution is to add the following words:

unique_ptr<T, D>::pointer's operations shall be well-formed, shall have well defined behavior, and shall not throw exceptions.

This leaves me with a big question : which operations?

Are all pointer operations required to be nothrow, including operations that have nothing to do with interactions with unique_ptr? This was much simpler with concepts where we could point to operations within a certain concept, and so nail down the interactions.

[ 2009-10-15 Daniel adds: ]

I volunteer to prepare a more fine-grained solution, but I would like to ask for feedback that helps me doing so. If this question is asked early in the meeting I might be able to fix it within the week, but I cannot promise that now.

[ 2009-10 Santa Cruz: ]

Leave in open. Daniel to provide wording as already suggested.

[ 2009-12-22 Daniel provided wording and rationale. ]

[ 2010 Pittsburgh: Moved to NAD Editorial. Rationale added below. ]

Rationale:

The here proposed resolution has considerable overlap with the requirements that are used in the allocator requirements.

This might be a convincing argument to isolate the common subset into one requirement. The reason I did not do that is basically because we might find out that they are either over-constraining or under-constraining at this late point of specification. Note also that as a result of the idea of a general requirement set I added the requirement:

A default-initialized object may have a singular value

even though this does not play a relevant role for unique_ptr.

One further characteristics of the resolution is that availability of relational operators of unique_ptr<T, D>::pointer is not part of the basic requirements, which is in sync with the allocator requirements on pointer-like (this means that unique_ptr can hold a void_pointer or const_void_pointer).

Solved by N3073.

Proposed resolution:

  1. Change 20.3.1.3 [unique.ptr.single] p. 1 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 this operator()]

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

  2. Change 20.3.1.3 [unique.ptr.single] p. 3 as indicated:

    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 satisfy the requirements of EqualityComparable, DefaultConstructible, CopyConstructible (Table 34) and, CopyAssignable (Table 36), Swappable, and Destructible (16.4.4.2 [utility.arg.requirements]). A default-initialized object may have a singular value. A value-initialized object produces the null value of the type. The null value shall be equivalent only to itself. An object of this type can be copy-initialized with a value of type nullptr_t, compared for equality with a value of type nullptr_t, and assigned a value of type nullptr_t. The effect shall be as if a value-initialized object had been used in place of the null pointer constant. An object p of this type can be contextually converted to bool. The effect shall be as if p != nullptr had been evaluated in place of p. No operation on this type which is part of the above mentioned requirements shall exit via an exception.

    [Note: Given an allocator type X (16.4.4.6 [allocator.requirements]), the types X::pointer, X::const_pointer, X::void_pointer, and X::const_void_pointer may be used as unique_ptr<T, D>::pointerend note]

    In addition to being available via inclusion of the <utility> header, the swap function template in 22.2.2 [utility.swap] is also available within the definition of unique_ptr's swap function.

  3. Change 20.3.1.3.2 [unique.ptr.single.ctor] p. 2+3 as indicated: [The first change ensures that we explicitly say, how the stored pointer is initialized. This is important for a constexpr function, because this may make a difference for user-defined pointer-like types]

    constexpr unique_ptr();
    

    ...

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

    3 - Postconditions: get() == 0 nullptr.

  4. Change 20.3.1.3.2 [unique.ptr.single.ctor] p. 6+7 as indicated: [This is a step-by-fix to ensure consistency to the changes of N2976]

    unique_ptr(pointer p);
    

    ...

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

    7 - Postconditions: get() == p. get_deleter() returns a reference to a default constructed value-initialized deleter D.

  5. Insert a new effects clause in 20.3.1.3.2 [unique.ptr.single.ctor] just before p. 14: [The intent is to fix the current lack of specification in which way the stored pointer is initialized]

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

    ...

    Effects: Constructs a unique_ptr which owns p, initializing the stored pointer with p and the 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.

  6. Change 20.3.1.3.2 [unique.ptr.single.ctor] p. 18+22 as indicated: [The intent is to clarify that the moved-from source must contain a null pointer, there is no other choice left]

    unique_ptr(unique_ptr&& u);
    

    [..]

    18 - Postconditions: get() == value u.get() had before the construction and u.get() == nullptr. get_deleter() returns a reference to the internally stored deleter which 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.

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

    [..]

    22 - Postconditions: get() == value u.get() had before the construction, modulo any required offset adjustments resulting from the cast from unique_ptr<U, E>::pointer to pointer and u.get() == nullptr. get_deleter() returns a reference to the internally stored deleter which was constructed from u.get_deleter().

  7. Change 20.3.1.3.2 [unique.ptr.single.ctor] p. 20 as indicated: [With the possibility of user-defined pointer-like types the implication does only exist, if those are built-in pointers. Note that this change should also be applied with the acceptance of 950]

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

    20 - Requires: If D 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. 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]

  8. Change 20.3.1.3.3 [unique.ptr.single.dtor] p. 2 as indicated:

    ~unique_ptr();
    

    ...

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

  9. Change 20.3.1.3.4 [unique.ptr.single.asgn] p. 3+8 as indicated: [The intent is to clarify that the moved-from source must contain a null pointer, there is no other choice left]

    unique_ptr& operator=(unique_ptr&& u);
    

    [..]

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

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

    [..]

    8 - Postconditions: This unique_ptr now owns the pointer which u owned, and u no longer owns it, u.get() == nullptr.

  10. Change 20.3.1.3.4 [unique.ptr.single.asgn] p. 6 as indicated: [With the possibility of user-defined pointer-like types the implication does only exist, if those are built-in pointers. Note that this change should also be applied with the acceptance of 950]

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

    [..]

    6 - Requires: Assignment of the deleter D from an rvalue D 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]

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

    unique_ptr& operator=(nullptr_t});
    

    11 - Effects: reset().

    12 - Postcondition: get() == 0 nullptr

  12. Change 20.3.1.3.5 [unique.ptr.single.observers] p. 1+4+12 as indicated:

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

    1 - Requires: get() != 0 nullptr. The variable definition add_lvalue_reference<T>::type t = *get() shall be well formed, shall have well-defined behavior, and shall not exit via an exception.

    [..]

    pointer operator->() const;
    

    4 - Requires: get() != 0 nullptr.

    [..]

    explicit operator bool() const;
    

    12 - Returns: get() != 0nullptr.

  13. Change 20.3.1.3.6 [unique.ptr.single.modifiers] p. 1 as indicated:

    pointer release();
    

    1 - Postcondition: get() == 0 nullptr.

  14. Change 20.3.1.3.6 [unique.ptr.single.modifiers] p. 9 as indicated: [The intent is to ensure that potentially user-defined swaps are used. A side-step fix and harmonization with the specification of the the deleter is realized. Please note the additional requirement in bullet 2 of this proposed resolution regarding the availability of the generic swap templates within the member swap function.]

    void swap(unique_ptr& u);
    

    8 - Requires: The deleter D shall be Swappable and shall not throw an exception under swap.

    9 - Effects: The stored pointers of *this and u are exchanged by an unqualified call to non-member swap. The stored deleters are swap'd (unqualified) exchanged by an unqualified call to non-member swap.

  15. Change 20.3.1.4.4 [unique.ptr.runtime.observers] p. 1 as indicated:

    T& operator[](size_t i) const;
    

    Requires: i < the size of the array to which the stored pointer points. The variable definition T& t = get()[i] shall be well formed, shall have well-defined behavior, and shall not exit via an exception.

  16. Change 20.3.1.4.5 [unique.ptr.runtime.modifiers] p. 1 as indicated:

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

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

  17. Change 20.3.1.6 [unique.ptr.special] as indicated: [We don't add the relational operators to the basic requirement set, therefore we need special handling here]

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

    Requires: The variable definition bool b = x.get() == y.get(); shall be well formed, shall have well-defined behavior, and shall not exit via an exception.

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

    Throws: nothing.

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

    Requires: The variable definition bool b = x.get() != y.get(); shall be well formed, shall have well-defined behavior, and shall not exit via an exception.

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

    Throws: nothing.

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

    Requires: The variable definition bool b = x.get() < y.get(); shall be well formed, shall have well-defined behavior, and shall not exit via an exception.

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

    Throws: nothing.

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

    Requires: The variable definition bool b = x.get() <= y.get(); shall be well formed, shall have well-defined behavior, and shall not exit via an exception.

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

    Throws: nothing.

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

    Requires: The variable definition bool b = x.get() > y.get(); shall be well formed, shall have well-defined behavior, and shall not exit via an exception.

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

    Throws: nothing.

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

    Requires: The variable definition bool b = x.get() >= y.get(); shall be well formed, shall have well-defined behavior, and shall not exit via an exception.

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

    Throws: nothing.