198

Why does this:

#include <string>
#include <iostream>
using namespace std;

class Sandbox
{
public:
    Sandbox(const string& n) : member(n) {}
    const string& member;
};

int main()
{
    Sandbox sandbox(string("four"));
    cout << "The answer is: " << sandbox.member << endl;
    return 0;
}

Give output of:

The answer is:

Instead of:

The answer is: four

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
Kyle
  • 4,487
  • 3
  • 29
  • 45
  • 50
    And just for more fun, if you had written `cout << "The answer is: " << Sandbox(string("four")).member << endl;`, then it would be guaranteed to work. –  May 11 '10 at 22:51
  • 8
    @RogerPate Could you explain why? – Paolo M Jun 08 '15 at 07:40
  • 25
    For someone who's curious, example Roger Pate posted works because *string("four")* is temporary and that temporary is destroyed **at the end of full expresion**, so in his example when `SandBox::member` is read, temporary string **is still alive**. – PcAF May 23 '16 at 15:03
  • 2
    The question is: *Since writing such classes is dangerous, is there a compiler warning against passing temporaries to such classes*, or *is there a design guideline (in Stroustroup?) that prohibits writing classes that store references?* A design guideline to store pointers instead of references would be better. – Grim Fandango Mar 14 '17 at 17:00
  • 1
    @PcAF: Could you please explain why the temporary `string("four")` is destroyed at the end of the full expression, and not after the `Sandbox` constructor exits? Potatoswatter's answer says *A temporary bound to a reference member in a constructor’s ctor-initializer (§12.6.2 [class.base.init]) persists until the constructor exits.* – Taylor Nichols Aug 17 '18 at 00:45
  • if i add cout<<"\nvalue of mem = "< – user1057741 Feb 15 '20 at 12:38
  • 2
    FWIW, I'm not able to reproduce the output of "The answer is:" on GCC or MSVC 2013. Does this typically need -O3 or something for it to show itself? – jrh May 26 '20 at 21:11
  • @jrh That's the problem when you are trying to reproduce undefined behavior :) – Daniel Langr Dec 13 '22 at 12:53
  • 1
    @GrimFandango _"is there a design guideline (in Stroustroup?) that prohibits writing classes that store references?"_ [C.12: Don’t make data members const or references](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c12-dont-make-data-members-const-or-references) – ScumCoder Feb 08 '23 at 08:02

6 Answers6

190

Only local const references prolong the lifespan.

The standard specifies such behavior in §8.5.3/5, [dcl.init.ref], the section on initializers of reference declarations. The reference in your example is bound to the constructor's argument n, and becomes invalid when the object n is bound to goes out of scope.

The lifetime extension is not transitive through a function argument. §12.2/5 [class.temporary]:

The second context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object to a subobject of which the temporary is bound persists for the lifetime of the reference except as specified below. A temporary bound to a reference member in a constructor’s ctor-initializer (§12.6.2 [class.base.init]) persists until the constructor exits. A temporary bound to a reference parameter in a function call (§5.2.2 [expr.call]) persists until the completion of the full expression containing the call.

Community
  • 1
  • 1
Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
  • 55
    You should also see GotW #88 for a more human-friendly explanation: http://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/ – Nathan Ernst May 06 '10 at 22:59
  • 1
    I think it would be clearer if the standard said "The second context is when a reference is bound to a prvalue". In OP's code you could say that `member` is bound to a temporary, because initializing `member` with `n` means to bind `member` to the same object `n` is bound to, and that is in fact a temporary object in this case. – M.M May 18 '16 at 03:39
  • 2
    @M.M There are cases where lvalue or xvalue initializers containing a prvalue will extend the prvalue. My proposal paper [P0066](http://wg21.link/P0066) reviews the state of affairs. – Potatoswatter May 20 '16 at 07:50
  • Right, so my suggesting is also bad as it forgets about binding to lvalue or xvalue members of temporary object which would extend. Seems like this is one of those things that's intuitively obvious but not so easy to formalize. – M.M May 20 '16 at 07:53
  • 1
    As of C++11, Rvalue references also prolong the life of a temporary without requiring a `const` quaifier. – GetFree Dec 11 '17 at 23:18
  • @GetFree To be sure, the standard never clearly specified that `const` was a requirement for lifetime extension. That paper P0066 (see link above) offers some history in the introduction section. – Potatoswatter Dec 11 '17 at 23:24
  • Run this example on Linux, compiled by g++ version 7.3.0, the output is expected as "The answer is: four". So, is it undefined behaviour? – KeNVin Favo Jan 29 '19 at 09:55
  • 3
    @KeNVinFavo yes, using a dead object is always UB – Potatoswatter Jan 29 '19 at 09:58
  • if i add cout<<"\nvalue of mem = "< – user1057741 Feb 15 '20 at 12:52
  • The modern (c++23) version of this standard quote is considerably more cumbersome: https://eel.is/c++draft/class.temporary#6 – Ofek Shilon Oct 27 '22 at 19:33
32

Here's the simplest way to explain what happened:

In main() you created a string and passed it into the constructor. This string instance only existed within the constructor. Inside the constructor, you assigned member to point directly to this instance. When when scope left the constructor, the string instance was destroyed, and member then pointed to a string object that no longer existed. Having Sandbox.member point to a reference outside its scope will not hold those external instances in scope.

If you want to fix your program to display the behavior you desire, make the following changes:

int main()
{
    string temp = string("four");    
    Sandbox sandbox(temp);
    cout << sandbox.member << endl;
    return 0;
}

Now temp will pass out of scope at the end of main() instead of at the end of the constructor. However, this is bad practice. Your member variable should never be a reference to a variable that exists outside of the instance. In practice, you never know when that variable will go out of scope.

What I recommend is to define Sandbox.member as a const string member; This will copy the temporary parameter's data into the member variable instead of assigning the member variable as the temporary parameter itself.

Squirrelsama
  • 5,480
  • 4
  • 28
  • 38
  • If I do this: `const string & temp = string("four"); Sandbox sandbox(temp); cout << sandbox.member << endl;` Will it still work? – Yves May 18 '16 at 03:27
  • @Thomas `const string &temp = string("four");` gives the same result as `const string temp("four");` , unless you use `decltype(temp)` specifically – M.M May 18 '16 at 03:42
  • @M.M Thanks a lot now I totally understand this question. – Yves May 18 '16 at 04:07
  • `However, this is bad practice.` - why? If both the temp and the containing object use automatic storage in the same scope, isn't it 100% safe? And if you don't do that, what would you do if the string is too large and so too expensive to copy? – max Jul 04 '17 at 04:54
  • 2
    @max, because the class does not enforce the passed in temporary to have the correct scope. It means that one day you may forgot about this requirement, pass invalid temporary value and the compiler won't warn you. – Alex Che Jul 20 '18 at 15:46
5

Technically speaking, this program isn't required to actually output anything to standard output (which is a buffered stream to begin with).

  • The cout << "The answer is: " bit will emit "The answer is: " into the buffer of stdout.

  • Then the << sandbox.member bit will supply the dangling reference into operator << (ostream &, const std::string &), which invokes undefined behavior.

Because of this, nothing is guaranteed to happen. The program may work seemingly fine or may crash without even flushing stdout -- meaning the text "The answer is: " would not get to appear on your screen.

Tanz87
  • 1,111
  • 11
  • 10
  • 3
    When there's UB, the entire program's behaviour is undefined - it doesn't just start at a particular point in the execution. So we can't say for certain that `"The answer is: "` will be written anywhere. – Toby Speight Oct 24 '18 at 14:34
3

It's clear from the other answers that class members don't prolong the life of a temporary beyond the constructor call. There are cases though were your API can "safely" assume that all const& objects passed to a class won't be temporaries, but references to well scoped objects.

If you don't want to create copies, what can you do to ensure UB doesn't creep into your code? The best tool you have is to safeguard the assumption that std::string const& passed to the constructor are not temporaries, by declaring as deleted the overload that accepts such temporaries:

#include <string>
#include <iostream>
using namespace std;

class Sandbox
{
public:
    Sandbox(const string& n) : member(n) {}
    
    Sandbox(string&&) = delete;
    // ^^^ This guy ;) 

    const string& member;
};

int main()
{
    Sandbox sandbox(string("four"));
    // Detect you're trying ^^^ to bind a 
    // reference to a temporary and refuse to compile

    return 0;
}

Demo

Nikos Athanasiou
  • 29,616
  • 15
  • 87
  • 153
0

Because your temporary string went out of scope once the Sandbox constructor returned, and the stack occupied by it was reclaimed for some other purposes.

Generally, you should never retain references long-term. References are good for arguments or local variables, never class members.

Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
-1

you're referring to something which has vanished. The following will work

#include <string>
#include <iostream>

class Sandbox
{

public:
    const string member = " "; //default to whatever is the requirement
    Sandbox(const string& n) : member(n) {}//a copy is made

};

int main()
{
    Sandbox sandbox(string("four"));
    std::cout << "The answer is: " << sandbox.member << std::endl;
    return 0;
}
pcodex
  • 1,812
  • 15
  • 16