Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Temporary lifetime and perfect forwarding constructor

Tags:

c++

c++11

I am having trouble understanding why the lifetime of temporaries bound to const reference parameters is cut short when there is a perfect forwarding constructor around. First of, what we know about temporaries bound to reference parameters: they last for the full expression:

A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of the full expression containing the call

However I found cases where this is not true (or I might simply misunderstand what a full expression is). Let's take a simple example, first we define an object with verbose constructors and destructors:

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

And an object wrapper B, which will be used for reference collapsing:

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; };
};

We can now use our wrapper to capture references on temporaries and use them in function calls, like so:

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

The program above prints what I would expect:

create A
create B
Using A
kill B
kill A

So far so good. Now let's move back to B and add a perfect forwarding 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 now gives:

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

Note that our A object was now killed before it was used, which is bad! Why did the lifetime of the temporary not get extended to the full call of foo in this case? Also, there is no other call to the desctructor of A, so there is no other instance of it.

I can see two possible explanations:

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

Could someone clarify what is going on here?

Does this mean that adding a forwarding constructor to a class could break backward compatibility in some cases, like it did for B?

You can find the full code here, with another test case crashing for references to strings.

like image 245
Thibaut Avatar asked Oct 21 '14 10:10

Thibaut


2 Answers

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

The member A const& value is bound to a temporary A, but that A is created in the mem-initializer-list of the constructor B<A const&>::B(U&&) so its lifetime is extended only for the duration of that member initialization [class.temporary]/5:

— A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits.

Note that a mem-initializer-list is the part after the colon in a 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 forwarding constructor to a class could break backward compatibility in some cases, like it did for B?

Yes. In this case it's difficult to see why the forwarding constructor would be necessary; it's certainly dangerous where you have a reference member that a temporary could be bound to.

like image 69
ecatmur Avatar answered Nov 15 '22 23:11

ecatmur


void foo(B<const A&> b);

foo( {123} );

is semantically equivalent to:

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

that for a non-explicit constructor is semantically equivalent to:

B<const A&> b{123};

going further, since your forwarding-constructor takes anything, it actually is initialized with int, not A:

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

That is, a temporary instance of A is created on the constructor's 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 A instance is destroyed after the B's construction is finished, and you end up with a dangling reference within the body of foo.

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

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

so it stays alive within foo, and the forwarding-constructor selected for B<const A&> is instantiated with a type A&&.

like image 26
Piotr Skotnicki Avatar answered Nov 15 '22 21:11

Piotr Skotnicki