2

I learned, long ago, that copy initialization creates a temporary which is then used to initialize the destination, though the latter copy constructor is optimized out; but the compiler still pretends to use it, checking for existence and allowed access.

I noticed Herb Sutter mention, in updated GOTW posts, that this is no longer true when auto is used.

Specifically, Herb (writing in 2013) still states the familiar rules in general:

If x is of some other type, conceptually the compiler first implicitly converts x to a temporary widget object… Note that I said “conceptually” a few times above. That’s because practically compilers are allowed to, and routinely do, optimize away the temporary — from GOTW #1

He later notes that when auto is used (in auto w = x;) that only a single copy constructor is called because there is no way that x needs to be converted first.

In the case of a function returning an iterator (so it’s an rvalue):

After all, as we saw in GotW #1, usually that extra = means the two-step “convert to a temporary then copy/move” of copy-initialization—but recall that doesn’t apply when using auto like this. … presto! there cannot be any need for a conversion and we directly construct i. —GOTW #2 (emphasis mine)

If the type returned from the function was exactly the same type as the variable being initialized with copy initialization, the rule would be that the temporary gets optimized out but it still checks access on the copy constructor. Herb is saying that this is not the case with auto, but direct initialization is used.

There are other examples where he seems to be saying (though not with rigorous precision) that when using auto you no longer have the compiler pretend to use the copy constructor.

What’s going on? Has the rule changed? Is this some additional feature of auto that all the presentations have missed mentioning?

JDługosz
  • 5,592
  • 3
  • 24
  • 45
  • Well, nice you're clarifying that. Though _copy elision_ already ruled for the majority of decent compilers before. – user0042 Aug 16 '17 at 20:49
  • I point out that before 17, optimizing out the temporary still required the copy constructor to logically be available. With the new standard-specified copy elision that is no longer the case. – JDługosz Aug 16 '17 at 20:55
  • 1
    May be you misunderstood my compliment. – user0042 Aug 16 '17 at 20:58

1 Answers1

7

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.

JDługosz
  • 5,592
  • 3
  • 24
  • 45