C++ tidbit #7: Lifetime Extension by Const Ref
Const-ref can bind to a temporary...
Good chance you already know that, and even that the lifetime of the temporary extends to that of the reference. To verify, check the calls in the generated code for :
void g() {
? const C& refC = createC();
? f();
} // ~C() called only here, after f(), when refC goes out of scope.
I imagine the rationale was something like:
And lo, another attempt to be cute resulted in a nest of C++ gotchas.
... Until it can't
What kind of lifetime extension happens here?
C createC();
const C& f1(const C& refC1) { return refC1; }
void f2();
void f3() {
const C& refC2 = f1( createC() );
f2() ;
}
The `C` generated by `createC` lives until f3() exits, right? Surely it goes only through const-refs, right?
Well, no.
From a language lawyer perspective, this is because the `createC`s return value is bound to `refC1`, and dies when that ref goes out of scope - when `f1` exits *.
From a compiler perspective, this is a practical necessity: the compiler needs to be able to generate code for `f3` even without visibility into `f1`s implementation. The only thing it can do is destroy `createC`s return value after the `f1` call had returned. `refC2` is in fact a dangling reference.
... And can't again.
Well, how about:
领英推荐
struct C { C(); ~C();}
struct E {
? ? const C& m_refC;
? ? E() : m_refC(C()) {}
};
void g();
void f() {
? ? E e; ? // m_refC extended?
? ? g();
};
Now there is no argument the temporary `C()` can bind to, but the temporary is destroyed upon returning from E() and the reference is left dangling almost upon creation. The standardese here was too complex for me to chase, but the bottom line is - a const-ref data member cannot extend temporary lifetimes. Ever.
Sorta Kinda "Why"
24y ago (!) Herb Sutter mentioned an interesting fact on this lifetime extension:
Q3: When the reference goes out of scope, which destructor gets called?
A3: The same destructor that would be called for the temporary object. It’s just being delayed.
To see this in action it's time to add in some inheritance, and inspect the calls in the generated code for g() :
struct B {?B(); virtual ~B(); };
struct D : B {?D(); ~D() override; };
D createD();
void f();
void g() {
? const B& refB = createD();
? f();
} // Which dtor is called here?
When refB goes out of scope the dtor that is called is not ~B(), but rather ~D()!
This surprised me initially but seems natural in hindsight: in general, refB going out of scope does not call any dtor. The dtor call here is only by virtue of lifetime-extension of an object known to be of type D, and so that is really the only natural dtor to call.
Returning now to the const-ref-member example above,
struct E
? ? const C& m_refC;
? ? E() : m_refC(C()) {}
};{
it should appear natural that `~E()` has no way of deciding what to do when destroying `m_refC`. Rememver `~E()` can be implemented in a different translation unit than `E()`, and it cannot know the type of the initializing temporary - or even that `m_refC` was bound to a temporary in the first place, which now needs to be somehow destroyed.
______________________________________
* I'm not certain about the standardese here, as the reference argument is returned directly and doesn't go out-of-scope in the normal sense (if it was an object it would not have been destroyed). Any insight is welcome!
Lecturer at the Academic College of Tel-Aviv-Yaffo, Co-Organizer of Core C++
2 年A great article, as always. About the destructor call on a temporary with extended lifetime, the point is that it would call the proper destructor, even without a virtual destructor, As Herb emphasize in the link you point to. So in the example that you present you can even change B and D not to have a virtual destructor, to emphasize this point.
A coincidence with publishing this article :) https://pvs-studio.com/en/blog/posts/1006/
Quantitative Trading / Influencer
2 年Use Rust maybe ?
Believer in democracy
2 年c++ has too many "gotcha" features :-( fwiw [[clang::lifetimebound]] sometimes helps evade these pitfalls. also, consider using const-pointer rather than const-reference: https://google.github.io/styleguide/cppguide.html#Inputs_and_Outputs