11

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?

12
  • Y{} is already a temporary, so std::move is superfluous. Commented Jul 17, 2018 at 10:57
  • @Jarod42 I guess OP uses it to avoid copy elision. Commented Jul 17, 2018 at 10:58
  • @Jarod42 edited, just to make the example a bit clearer. Commented Jul 17, 2018 at 11:00
  • I've tried it on the godbolt.org and it compiled fine in all compilers I've tested (latest MSVC, gcc and clang). Not sure what libstdc++ version it uses (also works with libc++), so it might be some "bug" in 8.1.1 (or in clang & 8.1.1 combo). Commented Jul 17, 2018 at 11:04
  • @Dan hm ok I tried clang HEAD and clang 6.0.1 with GCC libraries for 7.3.1 and 8.1.1. I guess I’ll have to set up an Ubuntu to test there as well. Commented Jul 17, 2018 at 11:07

1 Answer 1

6

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.

Sign up to request clarification or add additional context in comments.

2 Comments

So according to this (great!) analysis, it should be considered a bug in WebKit to use std::optional in this particular way, or a bug in the implementation of std::optional in libstdc++? In either case, I will work around this by downgrading to an older version of libstdc++, but I might let the WebKit devs know if the former is the case.
This exact example was discussed on the core reflector recently - see the thread titled "mandatory copy elision versus ctor delegation".

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.