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++?

// Example 1

string f() { return "abc"; }

void g() {
const string& s = f();
  cout << s << endl;    // can we still use the "temporary" object?
}

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.

This is nifty. Consider the following code:

// Example 3

Derived factory(); // construct a Derived object

void g() {
  const Base& b = factory(); // calls Derived::Derived here
  // … use b …
} // calls Derived::~Derived directly here — not Base::~Base + virtual dispatch!

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.

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

  1. another gotcha

    struct T {
    T& operator<<(int x) { /* keep some state*/}
    }

    const T& x = T() << 7 << 9; //binding const& to rvalue you say? or not?

    Dangerous and silent, both in gcc and msvc and probably other compilers…

  2. > 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?

  3. Hi Herb,

    Hopefully the thread is still alive)

    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?

    Thanks,
    Fedor

  4. Herb,

    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.

  5. Hello!
    Very Interesting post! Thank you for such interesting resource!
    PS: Sorry for my bad english, I’v just started to learn this language ;)
    See you!
    Your, Raiul Baztepo

  6. Arvind: As an earlier commenter (Omkar) mentioned, set the warning level to 4 and you’ll see it:

    warning C4239: nonstandard extension used : 'initializing' : conversion from 'std::string' to 'std::string &'
    A non-const reference may only be bound to an lvalue

  7. Hi,

    I tested the following code with Microsoft VS 2005,
    Version 8.0.50727.42 (RTM. 050727-4200)
    Microsoft .NET Framework
    Version 2.0.50727 SP1, Installed Edition: Professional

    but I didn’t get any warning regarding non-const reference “tempString”. Please help me.

    #include “stdafx.h”
    #include

    using namespace std;

    string getTempString() { return “abc”; }

    void g()
    {
    string& tempString = getTempString();
    cout << tempString.c_str() << endl; // can we still use the “temporary” object?
    }

    int _tmain(int argc, _TCHAR* argv[])
    {
    g();
    return 0;
    }

  8. Just a quick gotcha for this GotW:

    class A
    {
    A(A const&) {}

    public:
    A() {}

    };

    A const& a = A(); // Compile error.

    This produces an error message such as

    ‘A::A’ : cannot access private member declared in class ‘A’

    The point being that according to the standard, when assigning an rvalue to a const reference, the compiler is allowed to make a copy of the rvalue.

    So, whether or not the compiler avails itself of this, the copy constructor must be accesible.

  9. @1

    In this case, isn’t this an example of bad design to have a “const T& m_t” member? Because you’re not sure how it will be constructed.

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

    Holder h1(T()); //this is bad

    T t;
    Holder h2(t); //this is ok.

    How can the creator of Holder class be sure that its class would be correctly used?

  10. My mistake…Warning level should be 4 and not 3 for this warning to appear !

    Sorry for inconvenience caused!

    Regards
    Omkar

  11. Another GOTCHA, I forgot to mention yesterday…
    string g()
    {
    return “TEMPORARY”;
    }
    int _tmain(int argc, _TCHAR* argv[])
    {
    //const Base& b = f();
    //const
    string &s = g();

    return 0;
    }

    VS 2005 compiler(version details I mentioned in my last email), did not issue even a warning(“non standard extension used”).

  12. 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!

    Thanks for your valuable time!

    With best regards
    Omkar Shukla.

  13. 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.

  14. Hi,
    I have tried using Microsoft Visual Studio 2005,Version 8.0.50727.42 (RTM.050727-4200)
    Microsoft .NET Framework
    Version 2.0.50727 SP1, Installed Edition: Professional

    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…!
    ==========================================

    #include “stdafx.h”
    #include “iostream”
    using namespace std;

    class Base{
    int b;
    public:
    virtual ~Base()
    {
    cout << “Base”;
    }

    };
    class Derived:public Base
    {
    int d;
    public:
    ~Derived()
    {
    cout << “Derived”;
    }

    };
    Derived f(){
    return Derived();
    };

    int _tmain(int argc, _TCHAR* argv[])
    {
    const Base& b = f();

    return 0;
    }

  15. 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).

    class scoper {
        string name_;
    public:
        scoper(const string& name) : name_(name) { }
        ~scoper() { cout << "But I hardly even know " << name_ << endl; }
        string name() const { return name_; }
    };

    class holder {
        const scoper& scope_;
    public:
        holder(const scoper& scope)
            : scope_(scope)
        {
            cout << "Holding on to " << scope_.name() << endl;
        }
        ~holder() {    cout << "Letting go of " << scope_.name() << endl;    }
    };

    int main()
    {
        holder h(scoper("’Er")); // pass temporary to ctor
        cout << "What happens now?\n";
    }

    VisualC++ outputs (although I’m pretty sure this is undefined behaviour):
    Holding on to ‘Er
    But I hardly even know ‘Er 
    [too early (?)]
    What happens now?
    Letting go of
                           [note that the name is missing here]

  16. Hi, Lanzkron (cid-03e5952cd13f469f ), 
        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 ?
     thanks:)
  17. 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)".

  18. An exception to the behavior of virtual destructor, cool !
    But hope people don’t care too much about this. They should write "virtual ~Base()" as they ever did.
  19. Thanks for another GotW!  I wonder, given the fact that f() returns a non-const string, wouldn’t it be safe to cast away the constness from s?
      const string& s = f();
    const_cast<string&>(s) = "new value!";  // Okay?
  20. 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:

    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.Pedantry.:-)

Comments are closed.