Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 965
Niklas B. Avatar asked Jul 17 '18 10:07

Niklas B.


People also ask

Does STD move move constructor?

std::move is actually just a request to move and if the type of the object has not a move constructor/assign-operator defined or generated the move operation will fall back to a copy.

Why is std :: move necessary?

std::move itself does "nothing" - it has zero side effects. It just signals to the compiler that the programmer doesn't care what happens to that object any more. i.e. it gives permission to other parts of the software to move from the object, but it doesn't require that it be moved.

What does std:: optional?

The class template std::optional manages an optional contained value, i.e. a value that may or may not be present. A common use case for optional is the return value of a function that may fail.

Why do we need move constructors?

A move constructor enables the resources owned by an rvalue object to be moved into an lvalue without copying.


1 Answers

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.

like image 134
Barry Avatar answered Sep 29 '22 18:09

Barry