2900. The copy and move constructors of optional are not constexpr

Section: 22.5.3 [optional.optional] Status: C++17 Submitter: United States Opened: 2017-02-03 Last modified: 2020-09-06

Priority: Not Prioritized

View all other issues in [optional.optional].

View all issues with C++17 status.

Discussion:

Addresses US 111

The copy and move constructors of optional are not constexpr. However, the constructors taking a const T& or T&& are constexpr, and there is a precedent for having a constexpr copy constructor in 29.4.3 [complex]. The defaulted copy and move constructors of pair and tuple are also conditionally constexpr (see 20.4.2 [pairs.pair] p2 and 20.5.2.1 [tuple.cnstr] p2).

A strong motivating use-case is constexpr functions returning optional values. This issue was discovered while working on a library making heavy use of such.

Proposed change: Add constexpr to:

optional(const optional &);
optional(optional &&) noexcept(see below);

[2017-02-23, Casey comments and suggests wording]

This issue corresponds to NB comment US 111, which requests that the move and copy constructors of std::optional be declared constexpr. The PR simply suggests adding the constexpr specifier to the declarations of the constructors. The PR fails to specify the most important thing — and this has been a failing of Library in general — under what conditions is the thing that we've declared constexpr actually expected to be usable in constant expression context? (I think the proper standardese here is "under what conditions is the full expression of an initialization that would invoke these constructors a constant subexpression?")

It is, I believe, well-known that conforming implementations of optional<T> must store a T in a union to provide constexpr constructors that either do [optional(T const&)] or do not [optional()] initialize the contained T. A general implementation of optional's copy/move constructors must statically choose which union member, if any, to activate in each constructor. Since there is no way to change the active member of a union in a constant expression, and a constructor must statically choose a union member to activate (i.e., without being affected by the runtime state of the copy/move constructor's argument) it's not possible to implement a general constexpr copy/move constructor for optional.

It is, however, possible to copy/move construct a trivially copy constructible/trivially move constructible union in constexpr context, which effectively copies the union's object representation, resulting in a union whose active member is the same as the source union's. Indeed, at least two major implementations of optional (MSVC and libc++) already provide that behavior as a conforming optimization when T is a trivially copyable type. If we are to declare optional<T>'s copy and move constructors constexpr, we should additionally specify that those constructors are only required to have the "constexpr mojo" when T is trivially copyable. (Note that I suggest "trivially copyable" here rather than "trivially copy constructible or trivially move constructible" since the simpler requirement is simpler to implement, and I don't believe the more complicated requirement provides any additional benefit: I've never seen a trivially copy constructible or trivially move constructible type outside of a test suite that was not also trivially copyable.)

Previous resolution [SUPERSEDED]:

This wording is relative to N4618.

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

    	constexpr optional(const optional &);
    	constexpr optional(optional &&) noexcept(see below);
    
  2. Edit 22.5.3.2 [optional.ctor] paragraph as indicated:

    	constexpr optional(const optional &);
    

    and

    	constexpr optional(optional &&) noexcept(see below);
    

[2017-02-23, Marshall comments]

This is related to LWG 2745.

[2017-02-28, Kona, Casey comments and improves wording]

Amended PR per LWG discussion in Kona: replace the "is trivially copyable" requirement with the more specific is_trivially_copy/move_constructible<T>. LWG was concerned that tuple is a counter-example to the assumption that all three traits are equivalent for real-world types.

Previous resolution [SUPERSEDED]:

This wording is relative to N4640.

  1. Change the synopsis of class template optional in 22.5.3 [optional.optional] as follows:

    […]
    // 20.6.3.1, constructors
    constexpr optional() noexcept;
    constexpr optional(nullopt_t) noexcept;
    constexpr optional(const optional&);
    constexpr optional(optional&&) noexcept(see below);
    […]
    
  2. Modify 22.5.3.2 [optional.ctor] as indicated:

    constexpr optional(const optional& rhs);
    

    […]

    -6- Remarks: This constructor shall be defined as deleted unless is_copy_constructible_v<T> is true. If T is a trivially copyable type, this constructor shall be a constexpr constructor.

    constexpr optional(optional&& rhs) noexcept(see below);
    

    […]

    -10- 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. If T is a trivially copyable type, this constructor shall be a constexpr constructor.

[Kona 2017-02-27]

Accepted as Immediate to resolve NB comment.

Proposed resolution:

This wording is relative to N4640.

  1. Change the synopsis of class template optional in 22.5.3 [optional.optional] as follows:

    […]
    // 20.6.3.1, constructors
    constexpr optional() noexcept;
    constexpr optional(nullopt_t) noexcept;
    constexpr optional(const optional&);
    constexpr optional(optional&&) noexcept(see below);
    […]
    
  2. Modify 22.5.3.2 [optional.ctor] as indicated:

    constexpr optional(const optional& rhs);
    

    […]

    -6- Remarks: This constructor shall be defined as deleted unless is_copy_constructible_v<T> is true. If is_trivially_copy_constructible_v<T> is true, this constructor shall be a constexpr constructor.

    constexpr optional(optional&& rhs) noexcept(see below);
    

    […]

    -10- 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. If is_trivially_move_constructible_v<T> is true, this constructor shall be a constexpr constructor.