I examined the C++17 standard (n4659) and could not find any special mention of auto
in the section on initialization, nor anything about initialization in the section on auto
. So, I went back to basics and read the rules for initialization in detail. And boy, has it changed! In C++17, the meaning of copy-initialization is not what you learned earlier. The many answers here on SO, and tutorials that explain how the copy constructor is needed but then optimized out are all obsolete, and now wrong.
C++17 has changed the way temporaries are handled, in order to support more copy elision and to enable a way to explain mandatory copy elision. In a nutshell, prvalues are not temporaries that may be optimized away but are still logically copied (or moved) to their final home. Instead, prvalues are sort of in limbo, with no address and no real existence. If a temporary is actually needed, one is “materialized”. But the idea here is that the prvalue can be collapsed out and the code that creates a value (such as the return
statement) can be matched up with the final target, avoiding the creation of a temporary.
You can see that this directly affects the meaning of initialization, and by itself cuts out the old rule about a temporary in copy initialization.
So here’s the deal:
the new initialization rules
direct-initialization vs copy-initialization
This description covers the case where an object of class type is being defined. That is, not a reference, not a primitive type, etc. To keep the description simple, I’m not mentioning move constructors every place that copy constructor is mentioned.
C c1 ( exp() ); //exp returns a value, not a reference.
C c2 (v);
C c3 = exp();
C c4 = v;
We have direct initialization (c1
and c2
) and copy initialization (c3
and c4
). The list forms will be covered separately.
The following cases are covered in order of priority. Each rule is described assuming that earlier rules did not match already.
prvalue
If the source is a prvalue (pure rvalue) and already the right type, we get the match-up between the place that created the value and the final destination. That is, in c1
and c3
, the value created by the return
statement in exp
will be created directly in the variable. In underlying machine language, the caller determines where the return value will go and passes this to exp
as another parameter or a dedicated register or whatnot. Here, the address where c1
(or c3
) will go is passed for this purpose. It always did that (in optimized code); but logically the caller set up a temporary and then copied the temporary to c3
, but was allowed to optimize out the copy. Now, there is no temporary. The direct pipeline from value creation inside the function to the declared variable in the caller is part of the specification.
Note that this new reality affects prvalues in general. A prvalue (pure rvalue) is what you have when a function returns an object by value. The passage that explains this case (§11.6 ¶17.6.1) doesn’t even mention copy vs direct forms!
This means that the copy constructor is not involved. Assuming the function had some way to create the object (a different constructor or private access), you can create a variable when you don’t have an accessible copy constructor at all. This can be handy for factories, where you want to control how objects are created and the type is not copyable and not movable.
direct, and sometimes copy
In direct-initialization (c2
), the parameters are used to call a constructor. Being direct, explicit constructors are considered. This should be familiar enough.
In copy-initialization, if the value is the same class as the destination or even a derived class, the value is used as a parameter to a constructor. That is c4
where v
is also of class C
, or class D
derived from C
so it is-a C
. We suppose that would be the copy constructor that’s chosen, but you can have special constructors for derived types. Being copy-initialization, explicit constructors are ignored. The copy constructor can’t be explicit anyway. But you could have C::C(const D&)
that’s explicit!
copy
In remaining cases of copy initialization, some conversion is found. For c4
suppose v
is of type E
. The conversion functions are conversion operators defined in E (e.g. E::operator C()
) and non-explicit constructors in C (e.g. C::C(const E&)
). The result of the conversion is then used with direct-initialization. That sounds familiar… but here is the tricky part: the conversion function might produce a prvalue! A normal conversion operator will return by value, so the return value from that operator is constructed directly in c4
. It’s possible to write a conversion operator that returns a reference, and is thus an lvalue. But converting constructors are always prvalues. So, it appears that if a constructor is used then copy-initialization is just as direct as direct-initialization. But now it’s a phantom prvalue, not a temporary, that goes away.
list initialization
C c5 { v1,foo(),7 };
C c6 = { v1,foo(),7 };
C c7 = {exp()};
First of all, if C
has a special constructor for initialization lists, then that is used. None of the regular stuff applies.
Otherwise, it’s pretty much the same as before. There are a few differences (§11.6.4 ¶3.6):
Because of the new syntax, it’s possible to specify multiple arguments for a constructor in the copy-initialization form. For example, c5
and c6
both specify the same constructor arguments. In copy-initialization the explicit constructors are disallowed. Note that I wrote disallowed not ignored: In regular copy-initialization, explicit constructors are ignored and overload resolution uses only the non-explicit forms. But in copy-list-initialization, all constructors are used for overload resolution, but then if an explicit constructor is chosen you get an error (§16.3.1.7).
Another difference is that when using the list syntax, narrowing conversions (§11.6.4 ¶7) are flagged as errors if they would be used in implicit conversions.
Now for a big surprise: the temporary is back! in case c7
, even though exp()
is a prvalue, the rule is to match that to constructor arguments. So, the expression produced a value of type C
, but then the value is used to pick a constructor (which will be the copy constructor). Since the copy constructor cannot be explicit, there is no difference between the direct and copy syntax.
What does a normal programmer need to know?
This is all rather complex, but a normal day-to-day coder only needs to understand a few simple rules.
- prvalues are piped directly to the new home. So the details happened in the function return, and there really isn’t anything happening here. So, there is no meaningful distinction between copy and direct syntax.
- copy-initialization vs direct-initialization affects
explicit
constructors.
- direct-initialization specifically asks for a constructor; copy-initialization can use any way of converting.
for lists:
- list-initialization can use a special list constructor (e.g.
std::vector
).
- list-initialization gives constructor arguments, but doesn’t allow narrowing conversions.
So, there’s the new part about lists. But really, it’s something you need to unlearn: forget about the convert-then-(pretend to)copy business.