Scope of const references to subojects of temporaries

G

Guest

In the following example, section #3 fails under VC98, VC2003, VC2005 Express
Beta (Aug 2004) and g++ 3.3.2. Is this just a pitfall of the C++
specification? Why don't any of the above compilers at least flag this as a
warning as they would when say trying to return a const & to a local?

In Section #2, the const B& Bref is initialized and bound to the temporary
returned from GetSettings(). That is the temporary B exists until Bref goes
out of scope.

What appears to be happening in section #3 is this:

1. A temporary B object is copy constructed and returned from the
GetSettings() call.
2. The GetData() call on that temporary returns a const & and no temporary
is created for its return, thus Cref2 is initialized to a reference a member
of a temporary object.
3. The temporary B object goes out of scope.
4. The Cref2.Test() call is then made on an object that has passed out of
scope and no longer exists.

When the const C& Cref2 is initialized to refer to a subobject of the
temporary B shouldn't that also cause the temporaries scope to be bound to
that of Cref2?

This behavior was discovered when we changed a rather large class
hierarchy's A::GetSettings() from returning by const & to be a return by
value instead and things quit working correctly. What we thought was a
couple line code re-factor turned out to have this nasty consequence. So I
ask, is the compiler correctly implementing the C++ spec here (or are all the
ones we tested broken)? And, can the compiler produce an error or warning to
alert the programmer?

Thanks

-------snip below here-------

#include <cassert>
#include <iostream>
#include <vector>

using namespace std;

struct C
{
C() : mBuffer(100, 0xDC) {cerr << "C()\n";}
C(const C &c) : mBuffer(c.mBuffer) {cerr << "C(const C &)\n";}
virtual ~C() {mBuffer.clear(); cerr << "~C()\n";}

void Test() const {assert(!mBuffer.empty());}

protected:
vector<char> mBuffer;
};


struct B
{
B() {cerr << "B()\n";}
B(const B &b) : mData(b.mData) {cerr << "B(const B &)\n";}
virtual ~B() {cerr << "~B()\n";}

const C &GetData() const {return mData;}

protected:
C mData;
};


struct A
{
A() {cerr << "A()\n";}
A(const A &a) {cerr << "A(const A &)\n";}
virtual ~A() {cerr << "~A()\n";}

virtual B GetSettings() const {return mSettings;}

protected:
B mSettings;
};


int main(void)
{
A anObject;

//1. This works
anObject.GetSettings().GetData().Test();

//2. This works as well
const B &Bref = anObject.GetSettings();
const C &Cref = Bref.GetData();
Cref.Test();

/*
//3. This doesn't work...no compile warnings or errors, but assert pops
const C &Cref2 = anObject.GetSettings().GetData();
Cref2.Test();
*/

cerr << "End Scope of main()\n";
return 0;
}
 
T

Tom Widmer

ATASLO said:
In the following example, section #3 fails under VC98, VC2003, VC2005 Express
Beta (Aug 2004) and g++ 3.3.2. Is this just a pitfall of the C++
specification?

Yes, you only get lifetime extension when a temporary is directly bound
to a reference.

Why don't any of the above compilers at least flag this as a
warning as they would when say trying to return a const & to a local?

I think it's harder for the compiler to detect, at least in the general
case.
In Section #2, the const B& Bref is initialized and bound to the temporary
returned from GetSettings(). That is the temporary B exists until Bref goes
out of scope.

What appears to be happening in section #3 is this:

1. A temporary B object is copy constructed and returned from the
GetSettings() call.
2. The GetData() call on that temporary returns a const & and no temporary
is created for its return, thus Cref2 is initialized to a reference a member
of a temporary object.
3. The temporary B object goes out of scope.
4. The Cref2.Test() call is then made on an object that has passed out of
scope and no longer exists.

When the const C& Cref2 is initialized to refer to a subobject of the
temporary B shouldn't that also cause the temporaries scope to be bound to
that of Cref2?

Right, that's what's happening.
This behavior was discovered when we changed a rather large class
hierarchy's A::GetSettings() from returning by const & to be a return by
value instead and things quit working correctly. What we thought was a
couple line code re-factor turned out to have this nasty consequence. So I
ask, is the compiler correctly implementing the C++ spec here (or are all the
ones we tested broken)? And, can the compiler produce an error or warning to
alert the programmer?

Yes, and no. I don't know of any compiler that warns in this situation,
since it isn't a situation that can easily be detected at compile time.

Tom
 
G

Guest

Tom Widmer said:
Yes, you only get lifetime extension when a temporary is directly bound
to a reference.

From section 12.2 of the C++ standard:
"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 or until the end of the scope in which the
temporary is created, whichever comes first."

Doesn't that imply that if the object to which the reference is being bound
is a subobject of a temporary itself, then the entire temporary complete
object's (ie parent's) scope is extended to that of the reference as well.
 
D

Doug Harrison [MVP]

ATASLO said:
From section 12.2 of the C++ standard:
"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 or until the end of the scope in which the
temporary is created, whichever comes first."

Doesn't that imply that if the object to which the reference is being bound
is a subobject of a temporary itself, then the entire temporary complete
object's (ie parent's) scope is extended to that of the reference as well.

It doesn't just imply it; it directly states it. However, it doesn't apply
in your case, because you're not binding a temporary. You had:

/*
//3. This doesn't work...no compile warnings or errors, but assert pops
const C &Cref2 = anObject.GetSettings().GetData();
Cref2.Test();
*/

Now, B::GetData returns const C&, which is a reference, so you're binding a
reference. As for your other cases:

//1. This works
anObject.GetSettings().GetData().Test();

//2. This works as well
const B &Bref = anObject.GetSettings();
const C &Cref = Bref.GetData();
Cref.Test();

Case (1) works because it's all one big expression, so any temporaries
produced live until the end of the full-expression.

Case (2) works because A::GetSettings returns a B, which you're binding to
Bref, so the lifetime rule applies. Then you can call B::GetData and use the
C& returned as long as Bref is still in scope, because it's keeping the B
(which contains the C to which Cref is bound) alive.
 
G

Guest

Doug Harrison said:
It doesn't just imply it; it directly states it. However, it doesn't apply
in your case, because you're not binding a temporary. You had:

/*
//3. This doesn't work...no compile warnings or errors, but assert pops
const C &Cref2 = anObject.GetSettings().GetData();
Cref2.Test();
*/

Since the C that is being returned by reference is a subobject of a
temporary, shouldn't it therefore be classified as a temporary as well?
 
V

Victor Bazarov

ATASLO said:
:




Since the C that is being returned by reference is a subobject of a
temporary, shouldn't it therefore be classified as a temporary as well?

Subobjects relate to conversions from D to B& where const B& is what lives
on, and D is the type of the temporary. So, if in your example, C would
derive from Cbase publicly and you'd do

const Cbase & cb = Bref.GetData();

then, the actual C temporary would live on.

B is a temporary created during the evaluation of the expression. It only
lives until the full expression is evaluated. You could view it as the
argument to the operator. ("operator dot") function in that expression,
just before 'B::GetData()' is called.

At least, that's my take on it...

V
 
D

Doug Harrison [MVP]

ATASLO said:
Since the C that is being returned by reference is a subobject of a
temporary, shouldn't it therefore be classified as a temporary as well?

But how's the compiler to know that? Make the definition of the function
GetData non-inline, and all it sees is a function that returns a const C&.
For all the compiler knows, that C& might refer to an unrelated object. The
validity of the code can't depend on things like this. By "subobject of a
temporary", the compiler means a base class or non-static member variable,
and "binding it to a reference" means binding it directly to that reference,
not through some function call.
 
G

Guest

That's a fair point I hadn't considered. Changing the implementation of B to
the following solves the problem but isn't a very good solution:

struct B
{
B() : mData(C()) {cerr << "B()\n";}
B(const B &b) : mData(b.mData) {cerr << "B(const B &)\n";}
virtual ~B() {cerr << "~B()\n";}

const C &mData;
};
The above fails to compile on VC98, but does work on VC2003 and VC2005 Beta1.

Then the following code will work and not copy construct any tempory C
objects:

const C &Cref = anObject.GetSettings().mData;
Cref.Test();

This solution violates the whole data encapsulation principal though in my
mind. The original problem though basically boils down to C++ not being able
to guarantee that an object obtained through a chained series of calls is
usable outside the scope of that chained expression. This still seems like a
hole in the language to me, but I can see how it may be difficult to build
all the various checks into the compiler.
 
D

Doug Harrison [MVP]

ATASLO said:
That's a fair point I hadn't considered. Changing the implementation of B to
the following solves the problem but isn't a very good solution:

struct B
{
B() : mData(C()) {cerr << "B()\n";}

That's not safe, because the temporary C will be destroyed at the end of the
initialization of the reference mData.
B(const B &b) : mData(b.mData) {cerr << "B(const B &)\n";}

That may not be safe, either, because it's all too easy to bind a temporary
to a const reference. If the parameter b is a temporary, then mData is again
left to be a dangling reference, though here it lives a little longer than
above, throughout the initialization of the B and the full-expression it
appears in. My rule of thumb is to always make copies of const reference
parameters, or in this case, a subobject of the parameter.

Note that in a conformant compiler[*], you can't bind a temporary to a
non-const reference, so it may be acceptable for a class to keep a reference
to a non-const object. You should then comment the ctor with something like:

// Lifetime of x must exceed this object's lifetime.

This is usually enough to prevent accidents. As for the const reference
case, I'd recommend pass by value if possible, pointers (possibly reference
counted) if not.

[*] VC does allow the binding of temporaries to non-const references in many
cases for backward compatibility reasons. I haven't checked if Whidbey
closes this hole, but I hope it does.
virtual ~B() {cerr << "~B()\n";}

const C &mData;
};
The above fails to compile on VC98, but does work on VC2003 and VC2005 Beta1.

It may compile, but it'll blow up sooner or later as you begin to use the
dangling reference mData.
Then the following code will work and not copy construct any tempory C
objects:

const C &Cref = anObject.GetSettings().mData;
Cref.Test();

This solution violates the whole data encapsulation principal though in my
mind. The original problem though basically boils down to C++ not being able
to guarantee that an object obtained through a chained series of calls is
usable outside the scope of that chained expression. This still seems like a
hole in the language to me, but I can see how it may be difficult to build
all the various checks into the compiler.

FWIW, I don't know how any language that strives for efficiency and doesn't
use garbage collection could avoid this issue.
 
T

Tom Widmer

ATASLO said:
That's a fair point I hadn't considered. Changing the implementation of B to
the following solves the problem but isn't a very good solution:

struct B
{
B() : mData(C()) {cerr << "B()\n";}

That's covered in 12.2/5 - the temporary exists only until B's
constructor exits. After that, mData is a dangling reference, even if
the B object still exists.

Tom
 
T

Tom Widmer

ATASLO said:
:




Since the C that is being returned by reference is a subobject of a
temporary, shouldn't it therefore be classified as a temporary as well?

The issue is that inside the GetData method, "*this" is an lvalue, not a
temporary, so member variables are also lvalues. As a result, the rules
for binding an lvalue to a reference apply. Direct binding to a
subobject of a temporary is not happening. Here's an example where you
might have direct binding to a subobject:

#include <iostream>
using namespace std;

struct B
{
B(){cout << "B()\n";}
B(B const&){cout << "B(B const&)\n";}
~B(){cout << "~B()\n";}
};

struct A
{
A(){cout << "A()\n";}
A(A const&){cout << "A(const&)\n";}
~A(){cout << "~A()\n";}
B b;
};

int main()
{
B const& b = A().b; //possible binding to a subobject of a temporary
cout << "end of scope\n";
}

As you can see, b is bound to A().b, an rvalue. Unfortunately even in
this example you have no guarantee that the A() object will persist,
since b may bind to a copy of A().b (see 8.5.3/5) (in which case the
second temporary B object will persist but the first won't). On my
compilers, Comeau C++ extends the lifetime of the whole temporary A
object till the end of main. OTOH, GCC 3.4 instead binds b to a copy of
A().b, and thus destroys the A temporary (and original B subobject)
before the end of main.

Tom
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top