GotW #88: A Candidate For the “Most Important const”

A friend recently asked me whether Example 1 below is legal, and if so what it means. It led to a nice discussion I thought I’d post here. Since it was in close to GotW style already, I thought I’d do another honorary one after all these years… no, I have not made a New Year’s Resolution to resume writing regular GotWs. :-)

JG Questions

Q1: Is the following code legal C++?

A1: Yes.

This is a C++ feature… the code is valid and does exactly what it appears to do.

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, and thus avoids what would otherwise be a common dangling-reference error. In the example above, the temporary returned by f() lives until the closing curly brace. (Note this only applies to stack-based references. It doesn’t work for references that are members of objects.)

Does this work in practice? Yes, it works on all compilers I tried (except Digital Mars 8.50, so I sent a bug report to Walter to rattle his cage :-) and he quickly fixed it for the Digital Mars 8.51.0 beta).

Q2: What if we take out the const… is Example 2 still legal C++?

// Example 2

string f() { return "abc"; }

void g() {string& s = f(); // still legal? cout << s << endl;}

A2: No.

The "const" is important. The first line is an error and the code won’t compile portably with this reference to non-const, because f() returns a temporary object (i.e., rvalue) and only lvalues can be bound to references to non-const.

Note: Visual C++ does allow Example 2 but emits a "nonstandard extension used" warning by default. A conforming C++ compiler can always allow otherwise-illegal C++ code to compile and give it some meaning — hey, it could choose to allow inline COBOL if some kooky compiler writer was willing to implement that extension, maybe after a few too many Tequilas. For some kinds of extensions the C++ standard requires that the compiler at least emit some diagnostic to say that the code isn’t valid ISO C++, as this compiler does.

I once heard Andrei Alexandrescu give a talk on ScopeGuard (invented by Petru Marginean) where he used this C++ feature and called it "the most important const I ever wrote." And this brings us to the Guru Question, which highlights the additional subtlety that Andrei’s code deftly leveraged…

Guru Question

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.

Corollary: You can take a const Base& to a Derived temporary and it will be destroyed without virtual dispatch on the destructor call.

Does this work in practice on real compilers? Yes: Every compiler I have access to calls the correct Derived destructor, including even ancient Borland 5.5 and Visual C++ 6.0 (and Digital Mars, though DM calls the destructor at the wrong time, as noted above).

Andrei leverages this subtlety (of course) in his ScopeGuard implementation to avoid making the implementation classes’ destructors virtual at all, which is okay in that case because those classes otherwise have no need for one.

Updates:

08.01.02 to emphasize the feature applies to stack-based references, and mention Walter’s fix for DM.

08.02.05 to clarify Petru Marginean invented ScopeGuard.

Related

26 Responses

Gosh, dates are confusing on this site! Not only are all dates displayed without the year, but Herb appears to have updated this GOTW in the past. Unless I’m mistaken this is 2008, so the updates should read:

The friend should have read the recent book by Stephen Dewhurst. This behavior is mentioned there. Seems neat that you can get an object from an external method without having to wrap it with a smart pointer or to do heap allocation. Returning by value looks better than "void GetObject(Object & result)".

I think the output of your code does not be inconsistent with the rule( 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 ).

my understanding is below:

code section:

//….

h( scoper("Er") ); // holder( const scoper& scope );

// in this line actually actually there are three main steps:

// 1. bind a temporary object to the reference scope above .

// 2. bind the const reference scoper_ with the reference scoper above not that temporary object.

//….

explanation section:

I think the problem arises from the step 2. the C++ rule never specifies binding a const reference A to const ref B on the stack lengthens the lifetime of the temporary pointed by ref B to the lifetime of the reference A. am I right ?

That’s not the Guru question, the guru question what is the exception to this rule?The following code outputs something that (initially) surprised me.

Executive overview: If a temporary object is passed to a constructor (by const reference) and bound to a member const reference, the lifetime of the temporary isn’t expanded and the reference becomes a dangling reference (with no diagnostic).

I created a win32 console app to test bypassing the virtual dispatch bypass…It failed. Could you please point out the reason…Infact it even did not raise any warning!Attached is the code snippet of what exactly I did…!
==========================================

Omkar: Your program doesn’t actually test bypassing the virtual dispatch — but just remove “virtual” from ~Base and you’ll see it still gives the same output as it does now (“DerivedBase”). That shows that you are indeed bypassing virtual dispatch and hitting the Derived destructor, whether the Base destructor is virtual or not.

Hello Herb,
Thanks…Actually I was thinking other way round…I thought that the destructions sequence would always be hitting only the base and derived would never be called!

So, as RTTI/Vtbl mechanism serves the type identification at run time (or the dynamic object binding), I thought that since a non polymorphic base class’s destructor, is always invoked whenever using a “Base & b” = “a deiverd object”, I was thinking that const Base &b would be doing the same and only invoking base desrtuctor!

[…] which leads to a dangling reference. The 2nd version is much better in terms of safety because of a special rule that makes the compiler extend the life-time of the returned temporary in some cases. Instead of […]

Good post — this is a technique I use frequently to avoid unnecessarily copying objects returned from methods.

@Florin,

You said “How can the creator of Holder class be sure that its class would be correctly used?”.
I’m a fan of reference members for external dependencies passed in at construction time. The way I ensure that temporaries aren’t passed to the constructor is by requiring a pointer to be passed, not a reference:

class Holder {
Holder(const T* t): m_t(*t) {}
const T& m_t;
}

My general usage is that a reference is passed in if the method needs to modify it, or to avoid copying, but pass a pointer in if you need to store either pointer or reference access to the object.

I think there were a few examples of const reference member variables above, for the purpose to test virtual dispatch. That brought me to another question.

It seems that lifetime extension of an oblect assignment to constant reference works only within a scope where constant reference is declared.
On the other hand, why lifetime of an object assigned to a const reference member variable is not extended to a container’s lifetime?

> On the other hand, why lifetime of an object assigned to a const reference member variable is not extended to a container’s lifetime?

My guess is that the lifetime extension described in this article is computable lexically – i.e. the scope limit is lexical scope. Extending the lifetime to a container’s lifetime requires garbage collection as far as I can tell – there is no code location where a destructor call can be definitely inserted at compile time. Consider when two containers grab the same reference and in the program you delete one of the containers before the other by coin toss. Where then should the destructor call be inserted?