Home | Contact Us | FAQ | Search & Site Map | Link to Us
Sign In | Join | Other 45 Sites in Network
HomeAnnouncementsFree MagazinesWhite PapersSubmit Content
Discussion GroupsASP.NETWindows FormsLanguages.NET FrameworkVisual Studio.NET
Articles.NET FrameworkASP.NETToolsWindows Forms
.NET DirectoryOpen Source ProjectsUser GroupsWeb Resources
Related Topics
Visual Basic 6SQL ServerMS AccessOther DB ProductsMS Server ProductsMore Topics ...

.NET Forum / Languages / Managed C++ / January 2005

Tip: Looking for answers? Try searching our database.

Scope of const references to subojects of temporaries

Thread view: 
Enable EMail Alerts  Start New Thread
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

Rate this thread:







Free Magazines

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

Oracle MagazineNetwork ComputingComputer WorldBio-IT WorldeWeekInformation WeekInfosecurity
 
Sign In
Join
My Latest Posts
My Monitored Threads
My Blog
My Photo Gallery
My Profile
My Homepage

Start New Thread
Enable EMail Alerts
Rate this Thread



©2008 Advenet LLC   Privacy Policy - Terms of Use
This website includes both content owned or controlled by Advenet as well as content owned or controlled by third parties.