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:
B(T &&v)
instead of template <class U>B(U &&v)
solves the problem.{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.
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.
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&&
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With