C++ tidbit #7: Lifetime Extension by Const Ref

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:

  1. Binding a non-const reference to a temporary is almost certainly a bug - all modifications through it would be voided almost immediately, when the temporary is destroyed.
  2. So let's allow only const references to bind to a temporary. If that means we have to make sure the temporary is alive as long as it's accessible through the reference, then so be it.
  3. ...Maybe forbid all references from binding to temporaries? Nah, this is C++. Let's not pose more than that minimal restriction on our users.

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!

Amir Kirsh

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.

David LEVY

Quantitative Trading / Influencer

2 年

Use Rust maybe ?

Ayal Hitron

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

要查看或添加评论,请登录

Ofek Shilon的更多文章

  • Compiler Limitations #3/3

    Compiler Limitations #3/3

    Some examples, before the main point As discussed in the 1st post in this series, clang isn't able to properly express…

    3 条评论
  • C++ tidbit #10: `=default` Impact on Initialization

    C++ tidbit #10: `=default` Impact on Initialization

    Here's just one gotcha of dozens lurking within C++ initializations. The following is called 'value initialization':…

    3 条评论
  • C++ tidbit #9: nullptr_t doesn't behave like a pointer type?

    C++ tidbit #9: nullptr_t doesn't behave like a pointer type?

    Suppose you want to customize some behavior for compile-time nullptrs. As a concrete example let's take a comparison of…

    11 条评论
  • C++ tidbit #8: Damaging Default Destructor

    C++ tidbit #8: Damaging Default Destructor

    Special member functions are implicitly generated by the compiler if the user didn't provide them - constructors…

    9 条评论
  • C++ tidbit #6: Virtual functions and trivial copy

    C++ tidbit #6: Virtual functions and trivial copy

    Trivial Copyability In C++ terms an object is `trivially-copyable` if it is ok to memcpy it around. Containers (stl and…

  • C++ tidbit #5: const static members

    C++ tidbit #5: const static members

    struct S { static const int x = 0; }; int? n = S::x; const int m = S::x; -- link error: undefined reference to…

    2 条评论
  • C++ tidbit #4: Signed UB

    C++ tidbit #4: Signed UB

    All happy integers are alike, but every unhappy integer is unhappy in its own way Since the early computing days all…

    7 条评论
  • C++ tidbit #3: Contextual Conversion

    C++ tidbit #3: Contextual Conversion

    Warmup: explicit constructors When you mark a cast operator as explicit: struct C { explicit operator bool() {…

    1 条评论

社区洞察

其他会员也浏览了