I was exploring the ugly world of std::intializer_list
.
As far as I've understood from the standard:
§ 11.6.4:
- 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
And if we use Initializer List there are only two function calls: copy constructor + destructor call.
Copy Constructor in C++ A copy constructor is a member function that initializes an object using another object of the same class. In simple terms, a constructor which creates an object by initializing it with an object of the same class, which has been created previously is known as a copy constructor.
Member initializer list is the place where non-default initialization of these objects can be specified. For bases and non-static data members that cannot be default-initialized, such as members of reference and const-qualified types, member initializers must be specified.
A copy constructor can also be defined by a user; in this case, the default copy constructor is not called.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With