9

I was exploring the ugly world of std::intializer_list.

As far as I've understood from the standard:

§ 11.6.4:

  1. An object of type std::initializer_list is constructed from an initializer list as if the implementation generated and materialized (7.4) a prvalue of type “array of N const E”, where N is the number of elements in the initializer list. Each element of that array is copy-initialized with the corresponding element of the initializer list, and the std::initializer_list object is constructed to refer to that array. [ Note: A constructor or conversion function selected for the copy shall be accessible (Clause 14) in the context of the initializer list. — end note ] [...]

So, in case the type E is a class, I expect the copy constructor to be called.


The following class does not allow copy construction:

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

I am going to try to instantiate a std::initializer_list with this class.

#include <vector>

void foo() {
  std::vector<NonCopyable>{NonCopyable{}, NonCopyable{}};
}

With g++-8.2 -std=c++14 I get what I expect, compiler error:

error: use of deleted function 'NonCopyable::NonCopyable(const NonCopyable&)'.

Perfect!


However, the behaviour changes with the new standard.

Indeed, g++-8.2 -std=c++17 compiles.

Compiler Explorer Test


I thought it was because of the new requirement about copy elision provided by the new standard, at first.

However, changing the standard library implementation (keeping c++17) the error comes back:

clang-7 -std=c++17 -stdlib=libc++ fails:

'NonCopyable' has been explicitly marked deleted here NonCopyable(const NonCopyable&) = delete;

Compiler Explorer Test


So what am I missing?

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

2) Why libc++ implementation does not compile here?


Edit Please note that, in the example g++ -std=c++17 (which compiles), if I change the default constructor as "user defined":

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

the program does not compile anymore (not because of link error).

Compiler Explorer Example

BiagioF
  • 9,368
  • 2
  • 26
  • 50
  • "*changing the standard library implementation (keeping c++17)*" You're also changing *compilers*, so it's a lot less trivial than you're making it sound. – Nicol Bolas Feb 03 '19 at 00:03

2 Answers2

5

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 3
    1) Default ctor's user-providedness doesn't affect trivially copyable. However, libstdc++ is dispatching on trivial, not trivially copyable (which is https://gcc.gnu.org/bugzilla/show_bug.cgi?id=68350). 2) Deleting the copy assignment operator doesn't make the type non-trivially-copyable either, but libstdc++'s optimization is engaged via `std::copy`, and so it does not apply the optimization if the type is not copy-assignable. – T.C. Feb 03 '19 at 00:57
  • Finally, this is technically library-level UB. Anything is conforming. – T.C. Feb 03 '19 at 00:59
  • 1
    "*Note that this type is trivially copyable even though it has a deleted copy constructor!*" I'm pretty sure that this behavior is not legal in accord with the standard. Even if the copy constructor is never called (pursuant to the "as if" rule), `initializer_list` constructors for containers require that the `value_type` is CopyConstructible, which `NonCopyable` is not. Specifically, it requires [EmplaceConstructible from the value type of the range, which `NonCopyable` is not](https://timsong-cpp.github.io/cppwp/n4140/container.requirements#sequence.reqmts-4). – Nicol Bolas Feb 03 '19 at 06:40
  • @NicolBolas Yes, and the penalty for breaking a *Requires* is undefined behavior, see [res.on.required]. So it's perfectly legal for the implementation to ignore it and do anything they want instead. – T.C. Feb 03 '19 at 17:54
  • Ah, and I'll correct my 2): additionally deleting copy assignment does make it non-trivially-copyable in the current WP as a result of DR1734 (the move members are suppressed), but that DR doesn't appear to have been implemented in the current implementations. – T.C. Feb 03 '19 at 18:01
  • @T.C. Yeah I need to rewrite most of this... at least it's mostly in the right direction. – Barry Feb 03 '19 at 19:56
2

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982