Search code examples
c++webkitc++17option-type

Should you be able move from std::optional<T> where T has non-trivial constructors?


I'm trying to compile WebKit with clang, and I'm hitting compile errors due to what is essentially the following pattern:

#include <iostream>
#include <optional>

struct X {
    X() = default;
    X(const X& other) { }
};

struct Y {
    std::optional<X> x;;
};

int main() {
    Y foo;
    Y bar(std::move(foo));
}

So, they use std::optional<T> where T (in their case, WTF::Variant) has non-trivial copy/move constructors, and then use the std::optional move constructor. This compiles fine with GCC 8.1.1, but not with clang 6.0.1 (using GCC 8.1.1's libstdc++):

In file included from test.cpp:2:
/bin/../lib64/gcc/x86_64-pc-linux-gnu/8.1.1/../../../../include/c++/8.1.1/optional:276:9: error: call to implicitly-deleted copy constructor of 'std::_Optional_payload<X, true, true, true>'
      : _Optional_payload(__engaged
        ^                 ~~~~~~~~~
/bin/../lib64/gcc/x86_64-pc-linux-gnu/8.1.1/../../../../include/c++/8.1.1/optional:739:4: note: in instantiation of member function 'std::_Optional_payload<X, true, true, true>::_Optional_payload' requested here
        : _M_payload(__other._M_payload._M_engaged,
          ^
/bin/../lib64/gcc/x86_64-pc-linux-gnu/8.1.1/../../../../include/c++/8.1.1/optional:985:11: note: in instantiation of member function 'std::_Optional_base<X, false, false>::_Optional_base' requested here
    class optional
          ^
test.cpp:9:8: note: in implicit move constructor for 'std::optional<X>' first required here
struct Y {
       ^
test.cpp:15:7: note: in implicit move constructor for 'Y' first required here
    Y bar(std::move(foo));
      ^
/bin/../lib64/gcc/x86_64-pc-linux-gnu/8.1.1/../../../../include/c++/8.1.1/optional:288:24: note: copy constructor of '_Optional_payload<X, true, true, true>' is implicitly deleted because variant field '_M_payload' has a
      non-trivial copy constructor
          _Stored_type _M_payload;

Is this valid C++, or is WebKit broken and clang rightfully rejects this code?


Solution

  • Consider this class:

    struct X
    {
        X(int);
        X(X&&) = delete;
    
        // does this need to invoke the move constructor??
        X() : X(X(0)) { }
    };
    

    According to gcc, the answer is no: this delegates directly to X(int). According to clang, the answer is yes, and this fails compilation with:

    <source>:55:15: error: call to deleted constructor of 'X'    
            X() : X(X(0)) { }   
                  ^ ~~~~    
    <source>:52:9: note: 'X' has been explicitly marked deleted here    
            X(X&&) = delete;   
            ^
    

    This seems like potential for a core language issue, since on the one hand [class.base.init]/6 says:

    The target constructor is selected by overload resolution. Once the target constructor returns, the body of the delegating constructor is executed.

    That is, we specifically talk about picking a constructor and invoking it - which would surely be X(X&&) in this case. But on the other hand, the very next paragraph says that this is initialization:

    The expression-list or braced-init-list in a mem-initializer is used to initialize the designated subobject (or, in the case of a delegating constructor, the complete class object) according to the initialization rules of [dcl.init] for direct-initialization.

    and this looks a lot like the guaranteed copy elision example in [dcl.init]/17.6:

    If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object. [ Example: T x = T(T(T())); calls the T default constructor to initialize x. — end example ]

    I am not sure which interpretation is correct, but clang rejecting doesn't seem obviously wrong to me.


    Why is this example relevant? optional's move constructor in libstdc++, for types that are trivially destructible and trivially copy/move assignable (like your X) go through: this constructor:

      constexpr
      _Optional_payload(bool __engaged, _Optional_payload&& __other)
      : _Optional_payload(__engaged
              ? _Optional_payload(__ctor_tag<bool>{},
                          std::move(__other._M_payload))
              : _Optional_payload(__ctor_tag<void>{}))
      { }
    

    This type has implicitly deleted copy and move constructors, due to having a union with member that isn't trivially copy constructible. So if this delegating constructor has to call the implicit copy constructor (as clang thinks), this is ill-formed. If it doesn't have to, and just either calls one or the other delegated constructor, then this call is fine.