2451. [fund.ts.v2] optional<T> should 'forward' T's implicit conversions

Section: 5.3 [fund.ts.v2::optional.object] Status: TS Submitter: Geoffrey Romer Opened: 2014-10-31 Last modified: 2018-07-08

Priority: Not Prioritized

View all other issues in [fund.ts.v2::optional.object].

View all issues with TS status.

Discussion:

Addresses: fund.ts.v2

Code such as the following is currently ill-formed (thanks to STL for the compelling example):

optional<string> opt_str = "meow";

This is because it would require two user-defined conversions (from const char* to string, and from string to optional<string>) where the language permits only one. This is likely to be a surprise and an inconvenience for users.

optional<T> should be implicitly convertible from any U that is implicitly convertible to T. This can be implemented as a non-explicit constructor template optional(U&&), which is enabled via SFINAE only if is_convertible_v<U, T> and is_constructible_v<T, U>, plus any additional conditions needed to avoid ambiguity with other constructors (see N4064, particularly the "Odd" example, for why is_convertible and is_constructible are both needed; thanks to Howard Hinnant for spotting this).

In addition, we may want to support explicit construction from U, which would mean providing a corresponding explicit constructor with a complementary SFINAE condition (this is the single-argument case of the "perfect initialization" pattern described in N4064).

[2015-10, Kona Saturday afternoon]

STL: This has status LEWG, but it should be priority 1, since we cannot ship an IS without this.

TK: We assigned our own priorities to LWG-LEWG issues, but haven't actually processed any issues yet.

MC: This is important.

[2016-02-17, Ville comments and provides concrete wording]

I have prototype-implemented this wording in libstdc++. I didn't edit the copy/move-assignment operator tables into the new operator= templates that take optionals of a different type; there's a drafting note that suggests copying them from the existing tables.

[LEWG: 2016-03, Jacksonville]

Discussion of whether variant supports this. We think it does.

Take it for C++17.

Unanimous yes.

Proposed resolution:

This wording is relative to N4562.

  1. Edit 22.5.3 [optional.optional] as indicated:

    template <class T>
    class optional
    {
    public:
      typedef T value_type;
      
      // 5.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> constexpr optional(const optional<U>&);
      template <class U> constexpr optional(optional<U>&&);
      
      […]
      
      // 5.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 5.3.1 [fund.ts.v2::optional.object.ctor], insert new signature specifications after p33:

    [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>
    constexpr 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: 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, 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>
    constexpr 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: 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<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 5.3.3 [fund.ts.v2::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.