The code below illustrated my concern:
#include <iostream>
struct O
{
~O()
{
std::cout << "~O()\n";
}
};
struct wrapper
{
O const& val;
~wrapper()
{
std::cout << "~wrapper()\n";
}
};
struct wrapperEx // with explicit ctor
{
O const& val;
explicit wrapperEx(O const& val)
: val(val)
{}
~wrapperEx()
{
std::cout << "~wrapperEx()\n";
}
};
template<class T>
T&& f(T&& t)
{
return std::forward<T>(t);
}
int main()
{
std::cout << "case 1-----------\n";
{
auto&& a = wrapper{O()};
std::cout << "end-scope\n";
}
std::cout << "case 2-----------\n";
{
auto a = wrapper{O()};
std::cout << "end-scope\n";
}
std::cout << "case 3-----------\n";
{
auto&& a = wrapper{f(O())};
std::cout << "end-scope\n";
}
std::cout << "case Ex-----------\n";
{
auto&& a = wrapperEx{O()};
std::cout << "end-scope\n";
}
return 0;
}
See it live here.
It's said that auto&&
will extend the life-time of the temporary object, but I can't find the standard words on this rule, at least not in N3690.
The most relevant may be section 12.2.5 about temporary object, but not exactly what I'm looking for.
So, would auto&& life-time extension rule apply to all the temporary objects involved in the expression, or only the final result?
More specific, is a.val
guaranteed to be valid (non-dangling) before we reach the end-of-scope in case 1?
Edit: I updated the example to show more cases (3 & Ex).
You'll see that only in case 1 the lifetime of O is extended.
In the same way that a reference to const
does:
const auto& a = wrapper{O()};
or
const wrapper& a = wrapper{O()};
or also
wrapper&& a = wrapper{O()};
More specific, is
a.val
guaranteed to be valid (non-dangling) before we reach the end-of-scope in case 1?
Yes, it is.
There's (almost) nothing particularly important about auto
here. It's just a place holder for the correct type (wrapper
) which is deduced by the compiler. The main point is the fact that the temporary is bound to a reference.
For more details see A Candidate For the “Most Important const” which I quote:
Normally, a temporary object lasts only until the end of the full expression in which it appears. However, C++ deliberately specifies that binding a temporary object to a reference to const on the stack lengthens the lifetime of the temporary to the lifetime of the reference itself
The article is about C++ 03 but the argument is still valid: a temporary can be bound to a reference to const
(but not to a reference to non-const
). In C++ 11, a temporary can also be bound to an rvalue reference. In both cases, the lifetime of the temporary is extended to the lifetime of the reference.
The relevant parts of the C++11 Standard are exactly those referred in the OP, that is, 12.2 p4 and p5:
4 - There are two contexts in which temporaries are destroyed at a different point than the end of the full expression. The first context is [...]
5 - The second context is when a reference is bound to a temporary. [...]
(There are some exceptions in the bullet points following these lines.)
Update: (Following texasbruce's comment.)
The reason why the O
in case 2 has a short lifespan is that we have auto a = wrapper{O()};
(see, there's no &
here) and then the temporary is not bound to a reference. The temporary is, actually, copied into a
using the compiler generated copy-constructor. Therefore, the temporary doesn't have its lifetime expanded and dies at the end of the full expression in which it appears.
There's a danger in this particular example because wrapper::val
is a reference. The compiler generated copy-constructor of wrapper
will bind a.val
to the same object that the temporary's val
member is bound to. This object is also a temporary but of type O
. Then, when this latter temporary dies we see ~O()
on the screen and a.val
dangles!
Contrast case 2 with this:
std::cout << "case 3-----------\n";
{
O o;
auto a = wrapper{o};
std::cout << "end-scope\n";
}
The output is (when compiled with gcc using option -fno-elide-constructors
)
case 3-----------
~wrapper()
end-scope
~wrapper()
~O()
Now the temporary wrapper
has its val
member bound to o
. Notice that o
is not a temporary. As I said, a
is a copy of the wrapper
temporary and a.val
also binds to
o
. Before the scope ends the temporary wrapper
dies and we see the first ~wrapper()
on the screen.
Then the scope ends and we get end-scope
. Now, a
and o
must be destroyed in the reverse order of construction, hence we see ~wrapper()
when a
dies and finally ~O()
when it's o
's time. This shows that a.val
doesn't dangle.
(Final remark: I've used -fno-elide-constructors
to prevent a optimization related to copy-construction that would complicate the discussion here but this is another story.)
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