List initialization and copy elision - c++

Consider the following example:
#include <cstdlib>
struct A
{
A(int, char*){};
A(const A&){ printf("copy-ctor\n"); }
};
int main()
{
A x = A(5, nullptr);
}
According to 8.5.16 (of C++11 standard) the line
A x = A(5, nullptr);
is treated as
A x(A(5, nullptr));
(i.e. a temporary object of type A is created and passed to copy-ctor of type A to initialize an x). Then according to 12.8.31 compiler is allowed (but is not forced) to perform an optimization called "copy elision" to eliminate the creation of a temporary of type A which effectively makes that line of code to become
A x(5, nullptr);
(i.e. no temporaries created, no copy-ctors called).
Now, suppose I use a list-initialization in the example above like this:
A x = {5, nullptr}; // (1)
or
A x = A{5, nullptr}; // (2)
Can someone please quote the appropriate pharagraphs from C++11 standard that confirm or deny that (1) and/or (2) would always (i.e. not only when compiler can do "copy elision" optimization) be treated as
A x(5, nullptr);
(i.e. first constructor of A is being directly called, no temporaries created, no copying of objects of type A is performed).

This answer is apparently wrong, which surprised me to learn. See the comments. I think the first and fourth bullet points of [dcl.init.list]/3 are what mean (1) invokes a constructor (or performs aggregate init) directly, without a temporary.
There is nothing in the standard that guarantees that (1) and (2) avoid a temporary. They are both copy-initialization, (1) is copy-list-initialization as defined by [dcl.init.list] p1:
List-initialization can occur in direct-initialization or copy-initialization contexts; list-initialization in a direct-initialization context is called direct-list-initialization and list-initialization in a copy-initialization context is called copy-list-initialization.
In both cases it is copy-initialization, and [dcl.init] says that may involve a move (which can be elided).
8.5/14,15:
The initialization that occurs in the form
T x = a;
[...] is called copy-initialization.
The initialization that occurs in the forms
T x(a);
T x{a};
[...] is called direct-initialization.
If your compiler is not smart enough to always elide the temporary, then to ensure no temporary with list-initialization you can use direct-list-initialization, i.e.
A x{5, nullptr};

Related

Return from function using copy-list-initialization, no copy/move constructor needed - Where's it stated in C++ 11 standard?

struct A {
A() {}
A(const A&) = delete;
};
A foo() {
return {}; // Calls A()
}
struct B {
B(const B&) = delete;
};
B bar() {
return {}; // Aggregate initialization.
}
foo and bar both compile fine in C++11, because they use copy-list-initialization. There is no copy elision needed.
Where is it mentioned in the C++ standard that in such cases copy/move constructors are not needed?
I can see in [stmt.return] A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization (8.5.4) from the specified initializer list.
I cannot find the section that mentions that in such cases copy/move constructors are not needed.
This is a C++11 (and 14, and 17) issue where aggregate-initialization is allowed to bypass the copy-constructor check.
B is an aggregate class (in C++11/14/17)
You can verify that in C++17 with the std::is_aggregate type trait
You are using list-initialization
This performs aggregate initialization, which is treated slightly differently than a regular constructor
Per #NicolBolas' nice answer regarding [stmt.return], return {} is like performing copy-list-initialization (aggregate initialzation) of the returned object directly (as opposed to constructing and then returning)
If you had written return B() instead, the compiler would have rejected this code.
The bypass is fixed in C++20 (The compiler will reject your code) per P1008 since B is no longer an aggregate type (C++20 says that a class with any user-declared constructors is not an aggregate).
Where is it mentioned in the C++ standard that in such cases copy/move constructors are not needed?
It isn't. There is no need to mention such a thing because copy-list-initialization is not specified to do any copying or moving.
The behavior of return {...}; is defined as follows:
A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization
So this syntax invokes copy-list-initialization, with the braced-init-list being the initializer and the function's return value object being the object to be initialized. So it is functionally equivalent to T return_value_object = {...};.
[dcl.init.list]/3 explains the entire process of list-initialization, and nowhere does it say that the object to be initialized is copied or moved from some T (where T is the object type being initialized). No temporary object of type T is created or anything of the kind. The braced-init-list simply initializes the object.
So there is no need for a copy or move constructor on T unless the members of the braced-init-list would themselves require one (if you provided an object of type T as a member of the list, for example).
Note that this is not elision. Elision implies that a copy/move would have happened but was optimized out. List initialization doesn't have any copying or moving to optimize away to begin with.

Object Initialization Syntax in C++ ( T obj = {...} vs T obj{...} )

What is the difference between the two forms of initialization, T obj = {…} and T obj{…}?
I initially thought T obj = {…} was shorthand for T obj = T{…} where a temporary object is copied into our new object. This, although doesn't execute a copy constructor (copy elision), requires its existence and access to it. But when I blocked copy constructor access in this particular class by making the constructor private, there was no error.
This means that there is no copy mechanism involved. So what's the function of the '=' symbol?
I have referred to the following question but was dissatisfied because of the absence of an explanation:
Is C++11 Uniform Initialization a replacement for the old style syntax?
EDIT: On a similar note, is there a difference between int arr[]{…} and int arr[] = {…}? I am asking this to see if I can bring out the contrast between uniform initialization and list initialization.
These have almost exactly the same effect:
T x = { 1, 2, 3 };
T x { 1, 2, 3 };
Technically the version with = is called copy-list-initialization and the other version is direct-list-initialization but the behaviour of both of those forms is specified by the list-initialization behaviour.
The differences are:
If copy-list-initialization selects an explicit constructor then the code is ill-formed.
If T is auto, then:
copy-list-initialization deduces std::initializer_list<Type_of_element>
direct-list-initialization only allows a single element in the list, and deduces Type_of_element.
More information: Why does the standard differentiate between direct-list-initialization and copy-list-initialization?
If T is an array type then the above still applies; since array list initialization is always aggregate initialization, there is never a constructor selected and so the two versions are the same in all cases.
T obj = T{...} (excluding auto) is exactly the same as T obj{...}, since C++17, i.e. direct-list-initialization of obj. Prior to C++17 there was direct-list-initialization of a temporary, and then copy-initialization of obj from the temporary.
I think that comparing these two syntaxes is not your real question.
It seems to me that you are expecting C++17 elision to behave as did the pre-C++17 "optimisation" permitted by the standard and performed by many implementations.
In that "optimisation", though a copy constructor invocation could be elided, it had to be valid and accessible.
That is not the case with C++17 elision.
This is a true elision, whereby just writing T{} doesn't really create a T, but instead says "I want a T", and an actual temporary is "materialised" only if/when it needs to be.
Redundant utterances of this fact are effectively collapsed into one, so despite screaming "I want a T! I want a T! I want a T! I want a T!" the child still only gets one T in the end. 😉
So, in C++17, T obj = T{...} is literally equivalent to T obj{...}.
That explains the results you're seeing, and your confusion.
You may read more about this feature on cppreference.com; here's a snippet from the top of the page:
Mandatory elision of copy/move operations
Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible:
[..]
In the initialization of an object, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type [..]

c++11 initialization T{p, ...} vs T = {p, ...} [duplicate]

We know that T v(x); is called direct-initialization, while T v = x; is called copy-initialization, meaning that it will construct a temporary T from x that will get copied / moved into v (which is most likely elided).
For list-initialization, the standard differentiates between two forms, depending on the context. T v{x}; is called direct-list-initialization while T v = {x}; is called copy-list-initialization:
§8.5.4 [dcl.init.list] p1
[...] List-initialization can occur in direct-initialization or copy-initialization
contexts; list-initialization in a direct-initialization context is called direct-list-initialization and list-initialization in a copy-initialization context is called copy-list-initialization. [...]
However, there are only two more references each in the whole standard. For direct-list-initialization, it's mentioned when creating temporaries like T{x} (§5.2.3/3). For copy-list-initialization, it's for the expression in return statements like return {x}; (§6.6.3/2).
Now, what about the following snippet?
#include <initializer_list>
struct X{
X(X const&) = delete; // no copy
X(X&&) = delete; // no move
X(std::initializer_list<int>){} // only list-init from 'int's
};
int main(){
X x = {42};
}
Normally, from the X x = expr; pattern, we expect the code to fail to compile, because the move constructor of X is defined as deleted. However, the latest versions of Clang and GCC compile the above code just fine, and after digging a bit (and finding the above quote), that seems to be correct behaviour. The standard only ever defines the behaviour for the whole of list-initialization, and doesn't differentiate between the two forms at all except for the above mentioned points. Well, atleast as far as I can see, anyways.
So, to summarize my question again:
What is the use of splitting list-initialization into its two forms if they (apparently) do the exact same thing?
Because they don't do the exact same thing. As stated in 13.3.1.7 [over.match.list]:
In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.
In short, you can only use implicit conversion in copy-list-initialization contexts.
This was explicitly added to make uniform initialization not, um, uniform. Yeah, I know how stupid that sounds, but bear with me.
In 2008, N2640 was published (PDF), taking a look at the current state of uniform initialization. It looked specifically at the difference between direct initialization (T{...}) and copy-initialization (T = {...}).
To summarize, the concern was that explicit constructors would effectively become pointless. If I have some type T that I want to be able to be constructed from an integer, but I don't want implicit conversion, I label the constructor explicit.
Then somebody does this:
T func()
{
return {1};
}
Without the current wording, this will call my explicit constructor. So what good is it to make the constructor explicit if it doesn't change much?
With the current wording, you need to at least use the name directly:
T func()
{
return T{1};
}

Difference between direct-list-initialization and copy-list-initialization? [duplicate]

We know that T v(x); is called direct-initialization, while T v = x; is called copy-initialization, meaning that it will construct a temporary T from x that will get copied / moved into v (which is most likely elided).
For list-initialization, the standard differentiates between two forms, depending on the context. T v{x}; is called direct-list-initialization while T v = {x}; is called copy-list-initialization:
§8.5.4 [dcl.init.list] p1
[...] List-initialization can occur in direct-initialization or copy-initialization
contexts; list-initialization in a direct-initialization context is called direct-list-initialization and list-initialization in a copy-initialization context is called copy-list-initialization. [...]
However, there are only two more references each in the whole standard. For direct-list-initialization, it's mentioned when creating temporaries like T{x} (§5.2.3/3). For copy-list-initialization, it's for the expression in return statements like return {x}; (§6.6.3/2).
Now, what about the following snippet?
#include <initializer_list>
struct X{
X(X const&) = delete; // no copy
X(X&&) = delete; // no move
X(std::initializer_list<int>){} // only list-init from 'int's
};
int main(){
X x = {42};
}
Normally, from the X x = expr; pattern, we expect the code to fail to compile, because the move constructor of X is defined as deleted. However, the latest versions of Clang and GCC compile the above code just fine, and after digging a bit (and finding the above quote), that seems to be correct behaviour. The standard only ever defines the behaviour for the whole of list-initialization, and doesn't differentiate between the two forms at all except for the above mentioned points. Well, atleast as far as I can see, anyways.
So, to summarize my question again:
What is the use of splitting list-initialization into its two forms if they (apparently) do the exact same thing?
Because they don't do the exact same thing. As stated in 13.3.1.7 [over.match.list]:
In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.
In short, you can only use implicit conversion in copy-list-initialization contexts.
This was explicitly added to make uniform initialization not, um, uniform. Yeah, I know how stupid that sounds, but bear with me.
In 2008, N2640 was published (PDF), taking a look at the current state of uniform initialization. It looked specifically at the difference between direct initialization (T{...}) and copy-initialization (T = {...}).
To summarize, the concern was that explicit constructors would effectively become pointless. If I have some type T that I want to be able to be constructed from an integer, but I don't want implicit conversion, I label the constructor explicit.
Then somebody does this:
T func()
{
return {1};
}
Without the current wording, this will call my explicit constructor. So what good is it to make the constructor explicit if it doesn't change much?
With the current wording, you need to at least use the name directly:
T func()
{
return T{1};
}

Why does the standard differentiate between direct-list-initialization and copy-list-initialization?

We know that T v(x); is called direct-initialization, while T v = x; is called copy-initialization, meaning that it will construct a temporary T from x that will get copied / moved into v (which is most likely elided).
For list-initialization, the standard differentiates between two forms, depending on the context. T v{x}; is called direct-list-initialization while T v = {x}; is called copy-list-initialization:
§8.5.4 [dcl.init.list] p1
[...] List-initialization can occur in direct-initialization or copy-initialization
contexts; list-initialization in a direct-initialization context is called direct-list-initialization and list-initialization in a copy-initialization context is called copy-list-initialization. [...]
However, there are only two more references each in the whole standard. For direct-list-initialization, it's mentioned when creating temporaries like T{x} (§5.2.3/3). For copy-list-initialization, it's for the expression in return statements like return {x}; (§6.6.3/2).
Now, what about the following snippet?
#include <initializer_list>
struct X{
X(X const&) = delete; // no copy
X(X&&) = delete; // no move
X(std::initializer_list<int>){} // only list-init from 'int's
};
int main(){
X x = {42};
}
Normally, from the X x = expr; pattern, we expect the code to fail to compile, because the move constructor of X is defined as deleted. However, the latest versions of Clang and GCC compile the above code just fine, and after digging a bit (and finding the above quote), that seems to be correct behaviour. The standard only ever defines the behaviour for the whole of list-initialization, and doesn't differentiate between the two forms at all except for the above mentioned points. Well, atleast as far as I can see, anyways.
So, to summarize my question again:
What is the use of splitting list-initialization into its two forms if they (apparently) do the exact same thing?
Because they don't do the exact same thing. As stated in 13.3.1.7 [over.match.list]:
In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.
In short, you can only use implicit conversion in copy-list-initialization contexts.
This was explicitly added to make uniform initialization not, um, uniform. Yeah, I know how stupid that sounds, but bear with me.
In 2008, N2640 was published (PDF), taking a look at the current state of uniform initialization. It looked specifically at the difference between direct initialization (T{...}) and copy-initialization (T = {...}).
To summarize, the concern was that explicit constructors would effectively become pointless. If I have some type T that I want to be able to be constructed from an integer, but I don't want implicit conversion, I label the constructor explicit.
Then somebody does this:
T func()
{
return {1};
}
Without the current wording, this will call my explicit constructor. So what good is it to make the constructor explicit if it doesn't change much?
With the current wording, you need to at least use the name directly:
T func()
{
return T{1};
}