.NET Forum / Languages / Managed C++ / January 2005
Scope of const references to subojects of temporaries
|
|
Thread rating:  |
ATASLO - 13 Jan 2005 00:53 GMT 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; }
Tom Widmer - 13 Jan 2005 09:40 GMT > 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 [quoted text clipped - 14 lines] > 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 [quoted text clipped - 3 lines] > 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
ATASLO - 18 Jan 2005 06:25 GMT > > 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. 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.
Doug Harrison [MVP] - 18 Jan 2005 09:02 GMT >> > 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++ [quoted text clipped - 12 lines] >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.
 Signature Doug Harrison Microsoft MVP - Visual C++
ATASLO - 18 Jan 2005 22:51 GMT > >> > 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++ [quoted text clipped - 21 lines] > 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?
Victor Bazarov - 18 Jan 2005 23:09 GMT >>>>>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++ [quoted text clipped - 25 lines] > 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
Doug Harrison [MVP] - 19 Jan 2005 00:41 GMT >> 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: [quoted text clipped - 10 lines] >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.
 Signature Doug Harrison Microsoft MVP - Visual C++
ATASLO - 19 Jan 2005 05:33 GMT 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.
> 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&. [quoted text clipped - 3 lines] > and "binding it to a reference" means binding it directly to that reference, > not through some function call. Doug Harrison [MVP] - 19 Jan 2005 06:45 GMT >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: [quoted text clipped - 8 lines] >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.
 Signature Doug Harrison Microsoft MVP - Visual C++
Tom Widmer - 19 Jan 2005 12:25 GMT > 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
Tom Widmer - 19 Jan 2005 13:03 GMT >>>>>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++ [quoted text clipped - 25 lines] > 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
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|