Temporary life and ideal call divert constructor

I am having trouble understanding why the lifetime of temporary referenced constant reference parameters is shortened when there is an ideal redirect constructor. First, what do we know about time references to reference parameters: they are saved for full expression:

The time reference to the reference parameter in the function call (5.2.2) is maintained until the completion of the full expression containing the call

However, I found cases where this is incorrect (or I just don’t understand what the full expression is). Let's take a simple example: first we define an object with detailed constructors and destructors:

struct A { A(int &&) { cout << "create A" << endl; } A(A&&) { cout << "move A" << endl; } ~A(){ cout << "kill A" << endl; } }; 

And the wrapper of object B that will be used to collapse the link:

 template <class T> struct B { T value; B() : value() { cout << "new B" << endl; } B(const T &__a) : value(__a) { cout << "create B" << endl; } B(const B &p) = default; B(B && o) = default; ~B(){ cout << "kill B" << endl; }; }; 

Now we can use our wrapper to capture links on temporary pages and use them in function calls, for example:

 void foo(B<const A&> a){ cout << "Using A" << endl; } int main(){ foo( {123} ); } 

The program above prints what I expect:

 create A create B Using A kill B kill A 

So far so good. Now back to B and add the ideal call divert constructor for convertible types:

 template <class T> struct B { /* ... */ template <class U, class = typename enable_if<is_convertible<U, T>::value>::type> B(U &&v) : value(std::forward<U>(v)) { cout << "new forward initialized B" << endl; } }; 

Compiling the same code again gives:

 create A new forward initialized B kill A Using A kill B 

Note that our object A now been killed before its use, which is bad! Why, in this case, the temporary service life does not extend to a full call to foo ? In addition, there is no other call to handle A , so there is no other instance.

I see two possible explanations:

  • either the types are not what I think they are: changing the constructor of the moveable move to B(T &&v) instead of template <class U>B(U &&v) solves the problem.
  • or {123} not a subexpression of foo( {123} ) . The permutation {123} for A(123) also solves the problem, which makes me wonder if the parenthesis initializers are complete expressions.

Can anyone clarify what is going on here?

Does this mean that adding a transfer constructor to the class can lead to incorrect compatibility in some cases, for example, for B ?

Here you can find the full code, and another test case crashes for string references.

+7
c ++ c ++ 11
source share
2 answers

The type specified for U in the call B<A const&>::B(U&&) is int , so the only temporary that can be extended for life to call foo in main is the value prvalue int temporary is initialized to 123 .

Member A const& value bound to temporary A , but this A is created in the list of mem-initializer constructor of constructor B<A const&>::B(U&&) , therefore its lifetime extends only by the time of its initialization [class.temporary] / 5 :

- The temporary binding to the reference element in the ctor-initializer constructor (12.6.2) is maintained until the constructor completes.

Note that the mem-initializer list is the part after the colon in ctor-initializer:

  template <class U, class = typename enable_if<is_convertible<U, T>::value>::type> B(U &&v) : value(std::forward<U>(v)) { ^--- ctor-initializer ^--- reference member ^--- temporary A 

This is why kill A is printed after new forward initialized B

Does this mean that adding a transfer constructor to the class can lead to incorrect compatibility in some cases, for example, for B ?

Yes. In this case, it is difficult to understand why a redirect constructor is needed; this is certainly dangerous when you have a reference member with which the temporary can be associated.

+5
source share
 void foo(B<const A&> b); foo( {123} ); 

semantically equivalent:

 B<const A&> b = {123}; 

which for an implicit constructor is semantically equivalent:

 B<const A&> b{123}; 

goes further, since your forwarding constructor accepts something, it is actually initialized with int , not A :

 B<const A&>::B(int&& v) 

That is, a temporary instance of A is created in the constructor initialization list:

 B(int&& v) : value(A{v}) {} // created here ^ ^ destroyed here 

which is legal, just like you can type const A& a{123}; .

This instance of A destroyed after the completion of building B , and you get a sagging link in the body of foo .

The situation changes when you create an instance in the call expression, then temporary A ends its lifetime at the end of the call expression:

  foo( A{123} ); // ^ A is destroyed here 

therefore, it remains within foo , and the forwarding constructor selected for B<const A&> is created by an instance of type A&& .

+3
source share

All Articles