Copy Construction in Initializer Lists

The issue is that this type:

struct NonCopyable {
  NonCopyable() = default;   
  NonCopyable(const NonCopyable&) = delete;
};

is trivially copyable. So as an optimization, since std::initializer_list is just backed by an array, what libstdc++ is doing is simply memcpying the the whole contents into the vector as an optimization. Note that this type is trivially copyable even though it has a deleted copy constructor!

This is why when you make the default constructor user-provided (by just writing ; instead of = default;), is suddenly doesn't compile anymore. That makes the type no longer trivially copyable, and hence the memcpy path goes away.

As to whether or not this behavior is correct, I am not sure (I doubt there's a requirement that this code must not compile? I submitted 89164 just in case). You certainly want libstdc++ to take that path in the case of trivially copyable - but maybe it needs to exclude this case? In any case, you can accomplish the same by additionally deleting the copy assignment operator (which you probably want to do anyway) - that would also end up with the type not being trivially copyable.

This didn't compile in C++14 because you could not construct the std::initializer_list - copy-initialization there required the copy constructor. But in C++17 with guaranteed copy elision, the construction of std::initializer_list is fine. But the problem of actually constructing the vector is totally separate from std::initializer_list (indeed, this is a total red herring). Consider:

void foo(NonCopyable const* f, NonCopyable const* l) {
  std::vector<NonCopyable>(f, l);
}

That compiles in C++11 just fine... at least since gcc 4.9.


Does C++17 require copy-elision in the copy construction of elements of initializer_list?

Initializing the elements of an initializer_list never guaranteed the use of "copy construction". It merely performs copy initialization. And whether copy initialization invokes a copy constructor or not depends entirely on what is going on in the initialization.

If you have a type that is convertible from int, and you do Type i = 5;, that is copy initialization. But it will not invoke the copy constructor; it will instead invoke the Type(int) constructor.

And yes, the construction of the elements of the array the initializer_list references are susceptible to copy elision. Including C++17's rules for guaranteed elision.

That being said, what isn't susceptible to those rules is the initialization of the vector itself. vector must copy the objects from an initializer_list, so they must have an accessible copy constructor. How a compiler/library implementation manages to get around this is not known, but it is definitely off-spec behavior.