2756. C++ WP optional<T> should 'forward' T's implicit conversions

Section: 23.6.3 [optional.optional] Status: C++17 Submitter: Casey Carter Opened: 2016-07-26 Last modified: 2017-07-30

Priority: 1

View other active issues in [optional.optional].

View all other issues in [optional.optional].

View all issues with C++17 status.

Discussion:

LWG 2451 adds converting constructors and assignment operators to optional. The committee voted to apply it to the Library Fundamentals 2 TS WP in Oulu as part of LWG Motion 3. In both LWG and LEWG discussion of this issue, it was considered to be critical to apply to the specification of optional before shipping C++17 — it was an oversight on the part of LWG that there was no motion brought to apply this resolution to the C++ WP.

LWG 2745 proposes removal of the constexpr specifier from the declarations of the converting constructors from const optional<U>& and optional<U>&& since they are not implementable as constexpr constructors in C++17.

This issue proposes application of the resolution of LWG 2451 as amended by LWG 2745 to the specification of optional in the C++ WP.

[2016-07 — Chicago]

Monday: Priority set to 1; will re-review later in the week

[2016-08-03, Tomasz comments]

  1. Value forwarding constructor (template<typename U> optional(U&&)) is underspecified.

    For the following use code:

    optional<T> o1;
    optional<T> o2(o1);
    

    The second constructor will invoke value forwarding (U = optional<T>&) constructor (better match) instead of the optional<T> copy constructor, in case if T can be constructed from optional<T>. This happens for any type T that has unconstrained perfect forwarding constructor, especially optional<any>.

  2. The behavior of the construction of the optional<T> ot from optional<U> ou is inconsistent for classes T than can be constructed both from optional<U> and U. There are two possible semantics for such operation:

    For example, if we consider following class:

    struct Widget
    {
      Widget(int);
      Widget(optional<int>);
    };
    

    Notice, that such set of constructor is pretty common in situation when the construction of the Widget from known value is common and usage of optional version is rare. In such situation, presence of Widget(int) construction is an optimization used to avoid unnecessary empty checks and construction optional<int>.

    For the following declarations:

    optional<Widget> w1(make_optional(10));
    optional<Widget> w2;
    w2 = make_optional(10);
    

    The w1 will contain a value created using Widget(optional<int>) constructor, as corresponding unwrapping constructor (optional<U> const&) is eliminated by is_constructible_v<T, const optional<U>&> (is_constructible_v<Widget, const optional<int>&>) having a true value. In contrast w2 will contain a value created using Widget(int) constructor, as corresponding value forwarding assignment (U&&) is eliminated by the fact that std::decay_t<U> (optional<int>) is specialization of optional.

    In addition, the construction is having a preference for value forwarding and assignment is always using unwrapping. That means that for the following class:

    struct Thingy
    {
       Thingy(optional<string>);
    };
    
    optional<Thingy> t1(optional<string>("test"));
    

    The t1 has a contained value constructed from using Thingy(optional<std::string>), as unwrapping constructor (optional<U> const&) are eliminated by the is_constructible<T, U const&> (is_constructible<Thingy, std::string const&>) being false. However the following:

    t1 = optional<std::string>("test2");
    

    will not compile, because, the value forwarding assignment (U&&) is eliminated by std::decay_t<U> (optional<std::string>) being optional specialization and the unwrapping assignment (optional<U> const&) is ill-formed because is_constructible<T, U const&> (is_constructible<Thingy, std::string const&>) is false.

  3. The semantics of construction and assignment, of optional<optional<V>> from optional<U> where U is convertible to/ same as T is also inconsistent. Firstly, in this situation the optional<V> is a type that can be constructed both from optional<U> and U so it fails into set of problem described above. However in addition we have following non-template constructor in optional<T>:

    optional(T const&);
    

    Which for optional<optional<V>> is equivalent to:

    optional(optional<V> const&);
    

    While there is no corresponding non-template assignment from T const&, to make sure that o = {}; always clear an optional o.

    So for the following declarations:

    optional<int> oi;
    optional<short> os;
    
    optional<optional<int>> ooi1(oi);
    optional<optional<int>> ooi2(os);
    

    The ooi1 uses from-T constructor, while ooi2 uses value forwarding constructor. In this case both ooi1 and ooi2 are initialized and their contained values *ooi1, *ooi2 are uninitialized optionals. However, if we will attempt to make construction consistent with assignment, by preferring unwrapping (optional<U> const&), then ooi2 will end up being uninitialized.

    In summary, I believe that relation between unwrapping, value forwarding and from-T construction/assignment is to subtle to being handled as defect report and requires a full paper analyzing possible design and their pros/cons.

Tuesday PM: Ville and Eric to implement and report back in Issaquah. Moved to Open

Ville notes that 2746, 2754 and 2756 all go together.

Previous resolution [SUPERSEDED]:

This wording is relative to N4606.

  1. Modify 23.6.3 [optional.optional] as indicated:

    template <class T> class optional
    {
    public:
      using value_type = T;
      
      // 20.6.3.1, Constructors
      constexpr optional() noexcept;
      constexpr optional(nullopt_t) noexcept;
      optional(const optional &);
      optional(optional &&) noexcept(see below);
      constexpr optional(const T &);
      constexpr optional(T &&);
      template <class... Args> constexpr explicit optional(in_place_t, Args &&...);
      template <class U, class... Args>
        constexpr explicit optional(in_place_t, initializer_list<U>, Args &&...);
      template <class U> constexpr optional(U &&);
      template <class U> optional(const optional<U> &);
      template <class U> optional(optional<U> &&);
      
      […]
      
      // 20.6.3.3, Assignment
      optional &operator=(nullopt_t) noexcept;
      optional &operator=(const optional &);
      optional &operator=(optional &&) noexcept(see below);
      template <class U> optional &operator=(U &&);
      template <class U> optional& operator=(const optional<U> &);
      template <class U> optional& operator=(optional<U> &&);
      template <class... Args> void emplace(Args &&...);
      template <class U, class... Args>
        void emplace(initializer_list<U>, Args &&...);
    
      […]
      
    };
    
  2. In [optional.object.ctor], insert new signature specifications after p31:

    [Note: The following constructors are conditionally specified as explicit. This is typically implemented by declaring two such constructors, of which at most one participates in overload resolution. — end note]

    template <class U>
    constexpr optional(U&& v);
    

    -?- Effects: Initializes the contained value as if direct-non-list-initializing an object of type T with the expression std::forward<U>(v).

    -?- Postconditions: *this contains a value.

    -?- Throws: Any exception thrown by the selected constructor of T.

    -?- Remarks: If T's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor. This constructor shall not participate in overload resolution unless is_constructible_v<T, U&&> is true and U is not the same type as T. The constructor is explicit if and only if is_convertible_v<U&&, T> is false.

    template <class U>
    optional(const optional<U>& rhs);
    

    -?- Effects: If rhs contains a value, initializes the contained value as if direct-non-list-initializing an object of type T with the expression *rhs.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Throws: Any exception thrown by the selected constructor of T.

    -?- Remarks: This constructor shall not participate in overload resolution unless is_constructible_v<T, const U&> is true, is_same<decay_t<U>, T> is false, is_constructible_v<T, const optional<U>&> is false and is_convertible_v<const optional<U>&, T> is false. The constructor is explicit if and only if is_convertible_v<const U&, T> is false.

    template <class U>
    optional(optional<U>&& rhs);
    

    -?- Effects: If rhs contains a value, initializes the contained value as if direct-non-list-initializing an object of type T with the expression std::move(*rhs). bool(rhs) is unchanged.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Throws: Any exception thrown by the selected constructor of T.

    -?- Remarks: This constructor shall not participate in overload resolution unless is_constructible_v<T, U&&> is true, is_same<decay_t<U>, T> is false, is_constructible_v<T, optional<U>&&> is false and is_convertible_v<optional<U>&&, T> is false and U is not the same type as T. The constructor is explicit if and only if is_convertible_v<U&&, T> is false.

  3. In [optional.object.assign], change as indicated:

    template <class U> optional<T>& operator=(U&& v);
    

    -22- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, the state of v is determined by the exception safety guarantee of T's constructor. If an exception is thrown during the call to T's assignment, the state of *val and v is determined by the exception safety guarantee of T's assignment. The function shall not participate in overload resolution unless decay_t<U> is not nullopt_t and decay_t<U> is not a specialization of optionalis_same_v<decay_t<U>, T> is true.

    -23- Notes: The reason for providing such generic assignment and then constraining it so that effectively T == U is to guarantee that assignment of the form o = {} is unambiguous.

    template <class U> optional<T>& operator=(const optional<U>& rhs);
    

    -?- Requires: is_constructible_v<T, const U&> is true and is_assignable_v<T&, const U&> is true.

    -?- Effects:

    Table ? — optional::operator=(const optional<U>&) effects
    *this contains a value *this does not contain a value
    rhs contains a value assigns *rhs to the contained value initializes the contained value as if direct-non-list-initializing an object of type T with *rhs
    rhs does not contain a value destroys the contained value by calling val->T::~T() no effect

    -?- Returns: *this.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, the state of *rhs.val is determined by the exception safety guarantee of T's constructor. If an exception is thrown during the call to T's assignment, the state of *val and *rhs.val is determined by the exception safety guarantee of T's assignment. The function shall not participate in overload resolution unless is_same_v<decay_t<U>, T> is false.

    template <class U> optional<T>& operator=(optional<U>&& rhs);
    

    -?- Requires: is_constructible_v<T, U> is true and is_assignable_v<T&, U> is true.

    -?- Effects: The result of the expression bool(rhs) remains unchanged.

    Table ? — optional::operator=(optional<U>&&) effects
    *this contains a value *this does not contain a value
    rhs contains a value assigns std::move(*rhs) to the contained value initializes the contained value as if direct-non-list-initializing an object of type T with std::move(*rhs)
    rhs does not contain a value destroys the contained value by calling val->T::~T() no effect

    -?- Returns: *this.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, the state of *rhs.val is determined by the exception safety guarantee of T's constructor. If an exception is thrown during the call to T's assignment, the state of *val and *rhs.val is determined by the exception safety guarantee of T's assignment. The function shall not participate in overload resolution unless is_same_v<decay_t<U>, T> is false.

[2016-08-05 Chicago LWG]

Ville provides revised wording, that also fixes LWG 2753.

Rationale:

  1. The resolution of LWG 2753 makes special member functions defined as deleted in case the desired constraints aren't met.

  2. There is no decay for the converting constructor optional(U&&), there is a remove_reference instead. The target type may hold a cv-qualified type, and the incoming type may hold a cv-qualified type, but neither can hold a reference. Thus, remove_reference is what we need, remove_cv would be wrong, and decay would be wrong.

  3. There is no decay or remove_reference for converting constructors like optional(optional<U>), because none is needed.

  4. For optional(U&&), an added constraint is that U is not a specialization of optional

[2016-08, Chicago]

Fri PM: Move to Tentatively Ready

Previous resolution [SUPERSEDED]:

This wording is relative to N4606.

  1. Modify 23.6.3 [optional.optional] as indicated:

    template <class T> class optional
    {
    public:
      using value_type = T;
      
      // 20.6.3.1, Constructors
      constexpr optional() noexcept;
      constexpr optional(nullopt_t) noexcept;
      optional(const optional &);
      optional(optional &&) noexcept(see below);
      constexpr optional(const T &);
      constexpr optional(T &&);
      template <class... Args> constexpr explicit optional(in_place_t, Args &&...);
      template <class U, class... Args>
        constexpr explicit optional(in_place_t, initializer_list<U>, Args &&...);
      template <class U> EXPLICIT constexpr optional(U &&);
      template <class U> EXPLICIT optional(const optional<U> &);
      template <class U> EXPLICIT optional(optional<U> &&);
      
      […]
      
      // 20.6.3.3, Assignment
      optional &operator=(nullopt_t) noexcept;
      optional &operator=(const optional &);
      optional &operator=(optional &&) noexcept(see below);
      template <class U> optional &operator=(U &&);
      template <class U> optional& operator=(const optional<U> &);
      template <class U> optional& operator=(optional<U> &&);
      template <class... Args> void emplace(Args &&...);
      template <class U, class... Args>
        void emplace(initializer_list<U>, Args &&...);
    
      […]
      
    };
    
  2. Change [optional.object.ctor] as indicated:

    optional(const optional<T>& rhs);
    

    -3- Requires: is_copy_constructible_v<T> is true.

    […]

    -?- Remarks: This constructor shall be defined as deleted unless is_copy_constructible_v<T> is true.

    optional(optional<T>&& rhs) noexcept(see below);
    

    -7- Requires: is_move_constructible_v<T> is true.

    […]

    -11- Remarks: The expression inside noexcept is equivalent to is_nothrow_move_constructible_v<T>. This constructor shall be defined as deleted unless is_move_constructible_v<T> is true.

    constexpr optional(const T& v);
    

    -12- Requires: is_copy_constructible_v<T> is true.

    […]

    -16- Remarks: If T's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor. This constructor shall not participate in overload resolution unless is_copy_constructible_v<T> is true.

    constexpr optional(T&& v);
    

    -17- Requires: is_move_constructible_v<T> is true.

    […]

    -21- Remarks: If T's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor. This constructor shall not participate in overload resolution unless is_move_constructible_v<T> is true.

    template <class... Args> constexpr explicit optional(in_place_t, Args&&... args);
    

    -22- Requires: is_constructible_v<T, Args&&...> is true.

    […]

    -26- Remarks: If T's constructor selected for the initialization is a constexpr constructor, this constructor shall be a constexpr constructor. This constructor shall not participate in overload resolution unless is_constructible_v<T, Args...> is true.

    template <class U, class... Args>
      constexpr explicit optional(in_place_t, initializer_list<U> il, Args&&... args);
    

    -27- Requires: is_constructible_v<T, initializer_list<U>&, Args&&...> is true.

    […]

    -31- Remarks: The functionconstructor shall not participate in overload resolution unless is_constructible_v<T, initializer_list<U>&, Args&&...> is true. If T's constructor selected for the initialization is a constexpr constructor, this constructor shall be a constexpr constructor.

    [Note: The following constructors are conditionally specified as explicit. This is typically implemented by declaring two such constructors, of which at most one participates in overload resolution. — end note]

    template <class U>
      EXPLICIT constexpr optional(U&& v);
    

    -?- Effects: Initializes the contained value as if direct-non-list-initializing an object of type T with the expression std::forward<U>(v).

    -?- Postconditions: *this contains a value.

    -?- Throws: Any exception thrown by the selected constructor of T.

    -?- Remarks: If T's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor. This constructor shall not participate in overload resolution unless is_constructible_v<T, U&&> is true, remove_reference_t<U> is not the same type as T, and U is not a specialization of optional. The constructor is explicit if and only if is_convertible_v<U&&, T> is false.

    template <class U>
      EXPLICIT optional(const optional<U>& rhs);
    

    -?- Effects: If rhs contains a value, initializes the contained value as if direct-non-list-initializing an object of type T with the expression *rhs.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Throws: Any exception thrown by the selected constructor of T.

    -?- Remarks: This constructor shall not participate in overload resolution unless is_constructible_v<T, const U&> is true and is_same_v<U, T> is false. The constructor is explicit if and only if is_convertible_v<const U&, T> is false.

    template <class U>
      EXPLICIT optional(optional<U>&& rhs);
    

    -?- Effects: If rhs contains a value, initializes the contained value as if direct-non-list-initializing an object of type T with the expression std::move(*rhs). bool(rhs) is unchanged.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Throws: Any exception thrown by the selected constructor of T.

    -?- Remarks: This constructor shall not participate in overload resolution unless is_constructible_v<T, U&&> is true and is_same_v<U, T> is false. The constructor is explicit if and only if is_convertible_v<U&&, T> is false.

  3. Change [optional.object.assign] as indicated:

    optional<T>& operator=(const optional<T>& rhs);
    

    -4- Requires: is_copy_constructible_v<T> is true and is_copy_assignable_v<T> is true.

    […]

    -8- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's copy constructor, no effect. If an exception is thrown during the call to T's copy assignment, the state of its contained value is as defined by the exception safety guarantee of T's copy assignment. This operator shall be defined as deleted unless is_copy_constructible_v<T> is true and is_copy_assignable_v<T> is true.

    optional<T>& operator=(optional<T>&& rhs) noexcept(see below);
    

    -9- Requires: is_move_constructible_v<T> is true and is_move_assignable_v<T> is true.

    […]

    -13- Remarks: The expression inside noexcept is equivalent to:

    is_nothrow_move_assignable_v<T> && is_nothrow_move_constructible_v<T>
    

    -14- If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's move constructor, the state of *rhs.val is determined by the exception safety guarantee of T's move constructor. If an exception is thrown during the call to T's move assignment, the state of *val and *rhs.val is determined by the exception safety guarantee of T's move assignment. This operator shall be defined as deleted unless is_move_constructible_v<T> is true and is_move_assignable_v<T> is true.

    template <class U> optional<T>& operator=(U&& v);
    

    -15- Requires: is_constructible_v<T, U> is true and is_assignable_v<T&, U> is true.

    […]

    -19- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, the state of v is determined by the exception safety guarantee of T's constructor. If an exception is thrown during the call to T's assignment, the state of *val and v is determined by the exception safety guarantee of T's assignment. TheThis function shall not participate in overload resolution unless is_same_v<decay_t<U>, T> decay_t<U> is not nullopt_t, decay_t<U> is not a specialization of optional, is_constructible_v<T, U> is true and is_assignable_v<T&, U> is true.

    -20- Notes: The reason for providing such generic assignment and then constraining it so that effectively T == U is to guarantee that assignment of the form o = {} is unambiguous.

    template <class U> optional<T>& operator=(const optional<U>& rhs);
    

    -?- Effects:

    Table ? — optional::operator=(const optional<U>&) effects
    *this contains a value *this does not contain a value
    rhs contains a value assigns *rhs to the contained value initializes the contained value as if direct-non-list-initializing an object of type T with *rhs
    rhs does not contain a value destroys the contained value by calling val->T::~T() no effect

    -?- Returns: *this.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, the state of *rhs.val is determined by the exception safety guarantee of T's constructor. If an exception is thrown during the call to T's assignment, the state of *val and *rhs.val is determined by the exception safety guarantee of T's assignment. The function shall not participate in overload resolution unless is_constructible_v<T, const U&> is true and is_assignable_v<T&, const U&> is true and is_same_v<U, T> is false.

    template <class U> optional<T>& operator=(optional<U>&& rhs);
    

    -?- Effects: The result of the expression bool(rhs) remains unchanged.

    Table ? — optional::operator=(optional<U>&&) effects
    *this contains a value *this does not contain a value
    rhs contains a value assigns std::move(*rhs) to the contained value initializes the contained value as if direct-non-list-initializing an object of type T with std::move(*rhs)
    rhs does not contain a value destroys the contained value by calling val->T::~T() no effect

    -?- Returns: *this.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, the state of *rhs.val is determined by the exception safety guarantee of T's constructor. If an exception is thrown during the call to T's assignment, the state of *val and *rhs.val is determined by the exception safety guarantee of T's assignment. The function shall not participate in overload resolution unless is_constructible_v<T, U> is true and is_assignable_v<T&, U> is true and is_same_v<U, T> is false.

[2016-08-08 Ville reopens and provides improved wording]

This alternative proposed wording also resolves 2753.

The constructors that take a const T& or T&& are replaced by a constructor template that takes a U&& and defaults U = T. This allows copy-list-initialization with empty braces to still work:

optional<whatever> o = {}; // equivalent to initializing optional with nullopt

This resolution makes converting constructors and assignments have the same capabilities, including using arguments that can't be deduced. That is achieved by using a perfect-forwarding constructor and an assignment operator that default their argument to T. We don't need separate overloads for T, the overload for U does the job:

optional<vector<int>> ovi{{1, 2, 3}}; // still works
ovi = {4, 5, 6, 7}; // now works, didn't work before

Furthermore, this proposed wording makes optional "always unwrap". That is, the result of the following initializations is the same:

optional<optional<int>> oi = optional<int>();
optional<optional<int>> oi = optional<short>();

Both of those initializations initialize the optional wrapping another optional as if initializing with nullopt. Assignments do the same. These changes solve the issues pointed out by Tomasz Kamiński.

This P/R has been implemented and tested as a modification on top of libstdc++'s optional.

[2016-08-08 Ville and Tomasz collaborate and improve wording]

The suggested wording retains optional's converting constructors and assignment operators, but provides sane results for the types Tomasz Kaminski depicts in previous discussions.

As opposed to the current P/R of this issue, which does "always unwrap", this P/R does "always value-forward unless the incoming type is exactly a type that a special member function takes by reference, and don't unwrap if a value-forwarder can take an optional by any kind of reference".

I and Tomasz believe this is the best compromise between the different desires, and thus the best outcome so far.

This P/R has been implemented and tested on libstdc++.

[2016-09-08 Casey Carter finetunes existing resolution for move members]

[2016-09-09 Issues Resolution Telecon]

Move to Tentatively Ready

[2016-10-06 Ville Voutilainen finetunes the resolution for assignment from scalars]

Previous resolution [SUPERSEDED]:

This wording is relative to N4606.

  1. Modify 23.6.3 [optional.optional] as indicated:

    template <class T> class optional
    {
    public:
      using value_type = T;
      
      // 20.6.3.1, Constructors
      constexpr optional() noexcept;
      constexpr optional(nullopt_t) noexcept;
      optional(const optional &);
      optional(optional &&) noexcept(see below);
      constexpr optional(const T &);
      constexpr optional(T &&);
      template <class... Args> constexpr explicit optional(in_place_t, Args &&...);
      template <class U, class... Args>
        constexpr explicit optional(in_place_t, initializer_list<U>, Args &&...);
      template <class U = T> EXPLICIT constexpr optional(U &&);
      template <class U> EXPLICIT optional(const optional<U> &);
      template <class U> EXPLICIT optional(optional<U> &&);
      
      […]
      
      // 20.6.3.3, Assignment
      optional &operator=(nullopt_t) noexcept;
      optional &operator=(const optional &);
      optional &operator=(optional &&) noexcept(see below);
      template <class U = T> optional &operator=(U &&);
      template <class U> optional& operator=(const optional<U> &);
      template <class U> optional& operator=(optional<U> &&);
      template <class... Args> void emplace(Args &&...);
      template <class U, class... Args>
        void emplace(initializer_list<U>, Args &&...);
    
      […]
      
    };
    
  2. Change [optional.object.ctor] as indicated:

    optional(const optional<T>& rhs);
    

    -3- Requires: is_copy_constructible_v<T> is true.

    […]

    -?- Remarks: This constructor shall be defined as deleted unless is_copy_constructible_v<T> is true.

    optional(optional<T>&& rhs) noexcept(see below);
    

    -7- Requires: is_move_constructible_v<T> is true.

    […]

    -11- Remarks: The expression inside noexcept is equivalent to is_nothrow_move_constructible_v<T>. This constructor shall not participate in overload resolution unless is_move_constructible_v<T> is true.

    constexpr optional(const T& v);
    

    -12- Requires: is_copy_constructible_v<T> is true.

    -13- Effects: Initializes the contained value as if direct-non-list-initializing an object of type T with the expression v.

    -14- Postcondition: *this contains a value.

    -15- Throws: Any exception thrown by the selected constructor of T.

    -16- Remarks: If T's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor.

    constexpr optional(T&& v);
    

    -17- Requires: is_move_constructible_v<T> is true.

    -18- Effects: Initializes the contained value as if direct-non-list-initializing an object of type T with the expression std::move(v).

    -19- Postcondition: *this contains a value.

    -20- Throws: Any exception thrown by the selected constructor of T.

    -21- Remarks: If T's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor.

    template <class... Args> constexpr explicit optional(in_place_t, Args&&... args);
    

    -22- Requires: is_constructible_v<T, Args&&...> is true.

    […]

    -26- Remarks: If T's constructor selected for the initialization is a constexpr constructor, this constructor shall be a constexpr constructor. This constructor shall not participate in overload resolution unless is_constructible_v<T, Args...> is true.

    template <class U, class... Args>
      constexpr explicit optional(in_place_t, initializer_list<U> il, Args&&... args);
    

    -27- Requires: is_constructible_v<T, initializer_list<U>&, Args&&...> is true.

    […]

    -31- Remarks: The functionThis constructor shall not participate in overload resolution unless is_constructible_v<T, initializer_list<U>&, Args&&...> is true. If T's constructor selected for the initialization is a constexpr constructor, this constructor shall be a constexpr constructor.

    [Note: The following constructors are conditionally specified as explicit. This is typically implemented by declaring two such constructors, of which at most one participates in overload resolution. — end note]

    template <class U = T>
      EXPLICIT constexpr optional(U&& v);
    

    -?- Effects: Initializes the contained value as if direct-non-list-initializing an object of type T with the expression std::forward<U>(v).

    -?- Postconditions: *this contains a value.

    -?- Throws: Any exception thrown by the selected constructor of T.

    -?- Remarks: If T's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor. This constructor shall not participate in overload resolution unless is_constructible_v<T, U&&> is true, is_same_v<U, in_place_t> is false, and is_same_v<optional<T>, decay_t<U>> is false. The constructor is explicit if and only if is_convertible_v<U&&, T> is false.

    template <class U>
      EXPLICIT optional(const optional<U>& rhs);
    

    -?- Effects: If rhs contains a value, initializes the contained value as if direct-non-list-initializing an object of type T with the expression *rhs.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Throws: Any exception thrown by the selected constructor of T.

    -?- Remarks: This constructor shall not participate in overload resolution unless is_constructible_v<T, const U&> is true, is_constructible_v<T, optional<U>&> is false, is_constructible_v<T, const optional<U>&> is false, is_constructible_v<T, const optional<U>&&> is false, is_constructible_v<T, optional<U>&&> is false, is_convertible_v<optional<U>&, T> is false, is_convertible_v<const optional<U>&, T> is false, is_convertible_v<const optional<U>&&, T> is false, and is_convertible_v<optional<U>&&, T> is false. The constructor is explicit if and only if is_convertible_v<const U&, T> is false.

    template <class U>
      EXPLICIT optional(optional<U>&& rhs);
    

    -?- Effects: If rhs contains a value, initializes the contained value as if direct-non-list-initializing an object of type T with the expression std::move(*rhs). bool(rhs) is unchanged.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Throws: Any exception thrown by the selected constructor of T.

    -?- Remarks: This constructor shall not participate in overload resolution unless is_constructible_v<T, U&&> is true, is_constructible_v<T, optional<U>&> is false, is_constructible_v<T, const optional<U>&> is false, is_constructible_v<T, const optional<U>&&> is false, is_constructible_v<T, optional<U>&&> is false, is_convertible_v<optional<U>&, T> is false, is_convertible_v<const optional<U>&, T> is false, is_convertible_v<const optional<U>&&, T> is false, and is_convertible_v<optional<U>&&, T> is false. The constructor is explicit if and only if is_convertible_v<U&&, T> is false.

  3. Change [optional.object.assign] as indicated:

    optional<T>& operator=(const optional<T>& rhs);
    

    -4- Requires: is_copy_constructible_v<T> is true and is_copy_assignable_v<T> is true.

    […]

    -8- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's copy constructor, no effect. If an exception is thrown during the call to T's copy assignment, the state of its contained value is as defined by the exception safety guarantee of T's copy assignment. This operator shall be defined as deleted unless is_copy_constructible_v<T> is true and is_copy_assignable_v<T> is true.

    optional<T>& operator=(optional<T>&& rhs) noexcept(see below);
    

    -9- Requires: is_move_constructible_v<T> is true and is_move_assignable_v<T> is true.

    […]

    -13- Remarks: The expression inside noexcept is equivalent to:

    is_nothrow_move_assignable_v<T> && is_nothrow_move_constructible_v<T>
    

    -14- If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's move constructor, the state of *rhs.val is determined by the exception safety guarantee of T's move constructor. If an exception is thrown during the call to T's move assignment, the state of *val and *rhs.val is determined by the exception safety guarantee of T's move assignment. This operator shall not participate in overload resolution unless is_move_constructible_v<T> is true and is_move_assignable_v<T> is true.

    template <class U = T> optional<T>& operator=(U&& v);
    

    -15- Requires: is_constructible_v<T, U> is true and is_assignable_v<T&, U> is true.

    […]

    -19- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, the state of v is determined by the exception safety guarantee of T's constructor. If an exception is thrown during the call to T's assignment, the state of *val and v is determined by the exception safety guarantee of T's assignment. TheThis function shall not participate in overload resolution unless is_same_v<decay_t<U>, T> is_same_v<optional<T>, decay_t<U>> is false, is_constructible_v<T, U> is true, and is_assignable_v<T&, U> is true.

    -20- Notes: The reason for providing such generic assignment and then constraining it so that effectively T == U is to guarantee that assignment of the form o = {} is unambiguous.

    template <class U> optional<T>& operator=(const optional<U>& rhs);
    

    -?- Effects: See Table ?.

    Table ? — optional::operator=(const optional<U>&) effects
    *this contains a value *this does not contain a value
    rhs contains a value assigns *rhs to the contained value initializes the contained value as if direct-non-list-initializing an object of type T with *rhs
    rhs does not contain a value destroys the contained value by calling val->T::~T() no effect

    -?- Returns: *this.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, the state of *rhs.val is determined by the exception safety guarantee of T's constructor. If an exception is thrown during the call to T's assignment, the state of *val and *rhs.val is determined by the exception safety guarantee of T's assignment. This function shall not participate in overload resolution unless is_constructible_v<T, const U&> is true, is_assignable_v<T&, const U&> is true, is_constructible_v<T, optional<U>&> is false, is_constructible_v<T, const optional<U>&> is false, is_constructible_v<T, const optional<U>&&> is false, is_constructible_v<T, optional<U>&&> is false, is_convertible_v<optional<U>&, T> is false, is_convertible_v<const optional<U>&, T> is false, is_convertible_v<const optional<U>&&, T> is false, is_convertible_v<optional<U>&&, T> is false, is_assignable_v<T&, optional<U>&> is false, is_assignable_v<T&, const optional<U>&> is false, is_assignable_v<T&, const optional<U>&&> is false, and is_assignable_v<T&, optional<U>&&> is false.

    template <class U> optional<T>& operator=(optional<U>&& rhs);
    

    -?- Effects: See Table ?. The result of the expression bool(rhs) remains unchanged.

    Table ? — optional::operator=(optional<U>&&) effects
    *this contains a value *this does not contain a value
    rhs contains a value assigns std::move(*rhs) to the contained value initializes the contained value as if direct-non-list-initializing an object of type T with std::move(*rhs)
    rhs does not contain a value destroys the contained value by calling val->T::~T() no effect

    -?- Returns: *this.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, the state of *rhs.val is determined by the exception safety guarantee of T's constructor. If an exception is thrown during the call to T's assignment, the state of *val and *rhs.val is determined by the exception safety guarantee of T's assignment. This function shall not participate in overload resolution unless is_constructible_v<T, U> is true, is_assignable_v<T&, U> is true, is_constructible_v<T, optional<U>&> is false, is_constructible_v<T, const optional<U>&> is false, is_constructible_v<T, const optional<U>&&> is false, is_constructible_v<T, optional<U>&&> is false, is_convertible_v<optional<U>&, T> is false, is_convertible_v<const optional<U>&, T> is false, is_convertible<const optional<U>&&, T> is false, is_convertible<optional<U>&&, T> is false, is_assignable_v<T&, optional<U>&> is false, is_assignable_v<T&, const optional<U>&> is false, is_assignable_v<T&, const optional<U>&&> is false, and is_assignable_v<T&, optional<U>&&> is false.

Proposed resolution:

This wording is relative to N4606.

  1. Modify 23.6.3 [optional.optional] as indicated:

    template <class T> class optional
    {
    public:
      using value_type = T;
      
      // 20.6.3.1, Constructors
      constexpr optional() noexcept;
      constexpr optional(nullopt_t) noexcept;
      optional(const optional &);
      optional(optional &&) noexcept(see below);
      constexpr optional(const T &);
      constexpr optional(T &&);
      template <class... Args> constexpr explicit optional(in_place_t, Args &&...);
      template <class U, class... Args>
        constexpr explicit optional(in_place_t, initializer_list<U>, Args &&...);
      template <class U = T> EXPLICIT constexpr optional(U &&);
      template <class U> EXPLICIT optional(const optional<U> &);
      template <class U> EXPLICIT optional(optional<U> &&);
      
      […]
      
      // 20.6.3.3, Assignment
      optional &operator=(nullopt_t) noexcept;
      optional &operator=(const optional &);
      optional &operator=(optional &&) noexcept(see below);
      template <class U = T> optional &operator=(U &&);
      template <class U> optional& operator=(const optional<U> &);
      template <class U> optional& operator=(optional<U> &&);
      template <class... Args> void emplace(Args &&...);
      template <class U, class... Args>
        void emplace(initializer_list<U>, Args &&...);
    
      […]
      
    };
    
  2. Change [optional.object.ctor] as indicated:

    optional(const optional<T>& rhs);
    

    -3- Requires: is_copy_constructible_v<T> is true.

    […]

    -?- Remarks: This constructor shall be defined as deleted unless is_copy_constructible_v<T> is true.

    optional(optional<T>&& rhs) noexcept(see below);
    

    -7- Requires: is_move_constructible_v<T> is true.

    […]

    -11- Remarks: The expression inside noexcept is equivalent to is_nothrow_move_constructible_v<T>. This constructor shall not participate in overload resolution unless is_move_constructible_v<T> is true.

    constexpr optional(const T& v);
    

    -12- Requires: is_copy_constructible_v<T> is true.

    -13- Effects: Initializes the contained value as if direct-non-list-initializing an object of type T with the expression v.

    -14- Postcondition: *this contains a value.

    -15- Throws: Any exception thrown by the selected constructor of T.

    -16- Remarks: If T's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor.

    constexpr optional(T&& v);
    

    -17- Requires: is_move_constructible_v<T> is true.

    -18- Effects: Initializes the contained value as if direct-non-list-initializing an object of type T with the expression std::move(v).

    -19- Postcondition: *this contains a value.

    -20- Throws: Any exception thrown by the selected constructor of T.

    -21- Remarks: If T's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor.

    template <class... Args> constexpr explicit optional(in_place_t, Args&&... args);
    

    -22- Requires: is_constructible_v<T, Args&&...> is true.

    […]

    -26- Remarks: If T's constructor selected for the initialization is a constexpr constructor, this constructor shall be a constexpr constructor. This constructor shall not participate in overload resolution unless is_constructible_v<T, Args...> is true.

    template <class U, class... Args>
      constexpr explicit optional(in_place_t, initializer_list<U> il, Args&&... args);
    

    -27- Requires: is_constructible_v<T, initializer_list<U>&, Args&&...> is true.

    […]

    -31- Remarks: The functionThis constructor shall not participate in overload resolution unless is_constructible_v<T, initializer_list<U>&, Args&&...> is true. If T's constructor selected for the initialization is a constexpr constructor, this constructor shall be a constexpr constructor.

    [Note: The following constructors are conditionally specified as explicit. This is typically implemented by declaring two such constructors, of which at most one participates in overload resolution. — end note]

    template <class U = T>
      EXPLICIT constexpr optional(U&& v);
    

    -?- Effects: Initializes the contained value as if direct-non-list-initializing an object of type T with the expression std::forward<U>(v).

    -?- Postconditions: *this contains a value.

    -?- Throws: Any exception thrown by the selected constructor of T.

    -?- Remarks: If T's selected constructor is a constexpr constructor, this constructor shall be a constexpr constructor. This constructor shall not participate in overload resolution unless is_constructible_v<T, U&&> is true, is_same_v<U, in_place_t> is false, and is_same_v<optional<T>, decay_t<U>> is false. The constructor is explicit if and only if is_convertible_v<U&&, T> is false.

    template <class U>
      EXPLICIT optional(const optional<U>& rhs);
    

    -?- Effects: If rhs contains a value, initializes the contained value as if direct-non-list-initializing an object of type T with the expression *rhs.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Throws: Any exception thrown by the selected constructor of T.

    -?- Remarks: This constructor shall not participate in overload resolution unless is_constructible_v<T, const U&> is true, is_constructible_v<T, optional<U>&> is false, is_constructible_v<T, const optional<U>&> is false, is_constructible_v<T, const optional<U>&&> is false, is_constructible_v<T, optional<U>&&> is false, is_convertible_v<optional<U>&, T> is false, is_convertible_v<const optional<U>&, T> is false, is_convertible_v<const optional<U>&&, T> is false, and is_convertible_v<optional<U>&&, T> is false. The constructor is explicit if and only if is_convertible_v<const U&, T> is false.

    template <class U>
      EXPLICIT optional(optional<U>&& rhs);
    

    -?- Effects: If rhs contains a value, initializes the contained value as if direct-non-list-initializing an object of type T with the expression std::move(*rhs). bool(rhs) is unchanged.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Throws: Any exception thrown by the selected constructor of T.

    -?- Remarks: This constructor shall not participate in overload resolution unless is_constructible_v<T, U&&> is true, is_constructible_v<T, optional<U>&> is false, is_constructible_v<T, const optional<U>&> is false, is_constructible_v<T, const optional<U>&&> is false, is_constructible_v<T, optional<U>&&> is false, is_convertible_v<optional<U>&, T> is false, is_convertible_v<const optional<U>&, T> is false, is_convertible_v<const optional<U>&&, T> is false, and is_convertible_v<optional<U>&&, T> is false. The constructor is explicit if and only if is_convertible_v<U&&, T> is false.

  3. Change [optional.object.assign] as indicated:

    optional<T>& operator=(const optional<T>& rhs);
    

    -4- Requires: is_copy_constructible_v<T> is true and is_copy_assignable_v<T> is true.

    […]

    -8- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's copy constructor, no effect. If an exception is thrown during the call to T's copy assignment, the state of its contained value is as defined by the exception safety guarantee of T's copy assignment. This operator shall be defined as deleted unless is_copy_constructible_v<T> is true and is_copy_assignable_v<T> is true.

    optional<T>& operator=(optional<T>&& rhs) noexcept(see below);
    

    -9- Requires: is_move_constructible_v<T> is true and is_move_assignable_v<T> is true.

    […]

    -13- Remarks: The expression inside noexcept is equivalent to:

    is_nothrow_move_assignable_v<T> && is_nothrow_move_constructible_v<T>
    

    -14- If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's move constructor, the state of *rhs.val is determined by the exception safety guarantee of T's move constructor. If an exception is thrown during the call to T's move assignment, the state of *val and *rhs.val is determined by the exception safety guarantee of T's move assignment. This operator shall not participate in overload resolution unless is_move_constructible_v<T> is true and is_move_assignable_v<T> is true.

    template <class U = T> optional<T>& operator=(U&& v);
    

    -15- Requires: is_constructible_v<T, U> is true and is_assignable_v<T&, U> is true.

    […]

    -19- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, the state of v is determined by the exception safety guarantee of T's constructor. If an exception is thrown during the call to T's assignment, the state of *val and v is determined by the exception safety guarantee of T's assignment. TheThis function shall not participate in overload resolution unless is_same_v<decay_t<U>, T> is_same_v<optional<T>, decay_t<U>> is false, conjunction_v<is_scalar<T>, is_same<T, decay_t<U>>> is false, is_constructible_v<T, U> is true, and is_assignable_v<T&, U> is true.

    -20- Notes: The reason for providing such generic assignment and then constraining it so that effectively T == U is to guarantee that assignment of the form o = {} is unambiguous.

    template <class U> optional<T>& operator=(const optional<U>& rhs);
    

    -?- Effects: See Table ?.

    Table ? — optional::operator=(const optional<U>&) effects
    *this contains a value *this does not contain a value
    rhs contains a value assigns *rhs to the contained value initializes the contained value as if direct-non-list-initializing an object of type T with *rhs
    rhs does not contain a value destroys the contained value by calling val->T::~T() no effect

    -?- Returns: *this.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, the state of *rhs.val is determined by the exception safety guarantee of T's constructor. If an exception is thrown during the call to T's assignment, the state of *val and *rhs.val is determined by the exception safety guarantee of T's assignment. This function shall not participate in overload resolution unless is_constructible_v<T, const U&> is true, is_assignable_v<T&, const U&> is true, is_constructible_v<T, optional<U>&> is false, is_constructible_v<T, const optional<U>&> is false, is_constructible_v<T, const optional<U>&&> is false, is_constructible_v<T, optional<U>&&> is false, is_convertible_v<optional<U>&, T> is false, is_convertible_v<const optional<U>&, T> is false, is_convertible_v<const optional<U>&&, T> is false, is_convertible_v<optional<U>&&, T> is false, is_assignable_v<T&, optional<U>&> is false, is_assignable_v<T&, const optional<U>&> is false, is_assignable_v<T&, const optional<U>&&> is false, and is_assignable_v<T&, optional<U>&&> is false.

    template <class U> optional<T>& operator=(optional<U>&& rhs);
    

    -?- Effects: See Table ?. The result of the expression bool(rhs) remains unchanged.

    Table ? — optional::operator=(optional<U>&&) effects
    *this contains a value *this does not contain a value
    rhs contains a value assigns std::move(*rhs) to the contained value initializes the contained value as if direct-non-list-initializing an object of type T with std::move(*rhs)
    rhs does not contain a value destroys the contained value by calling val->T::~T() no effect

    -?- Returns: *this.

    -?- Postconditions: bool(rhs) == bool(*this).

    -?- Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, the state of *rhs.val is determined by the exception safety guarantee of T's constructor. If an exception is thrown during the call to T's assignment, the state of *val and *rhs.val is determined by the exception safety guarantee of T's assignment. This function shall not participate in overload resolution unless is_constructible_v<T, U> is true, is_assignable_v<T&, U> is true, is_constructible_v<T, optional<U>&> is false, is_constructible_v<T, const optional<U>&> is false, is_constructible_v<T, const optional<U>&&> is false, is_constructible_v<T, optional<U>&&> is false, is_convertible_v<optional<U>&, T> is false, is_convertible_v<const optional<U>&, T> is false, is_convertible<const optional<U>&&, T> is false, is_convertible<optional<U>&&, T> is false, is_assignable_v<T&, optional<U>&> is false, is_assignable_v<T&, const optional<U>&> is false, is_assignable_v<T&, const optional<U>&&> is false, and is_assignable_v<T&, optional<U>&&> is false.