Lifetime extension misconception

F

Frederick Gotham

There is a common misconception, (one which I myself also held at one point),
that a const reference can "extend the lifetime of a temporary". Examples
such as the following are given:

Snippet (1)
-----------

#include <string>
using std::string;

#include <iostream>
using std::cout;

string Func()
{
return "Hello";
}

int main()
{
string const &str = Func();

cout << str << '\n';
}

In the above snippet, no temporary "has had its lifetime extended". If such a
thing were true, then the following code would be perfectly OK:

Snippet (2):
------------

#include <string>
using std::string;

string Func()
{
return "Hello";
}

int main()
{
string const &cstr = Func();

string &str = const_cast<string&>(cstr);

str = "World";
}


But alas, Snippet (2) exhibits undefined behaviour, because the reference
does not refer to the original non-const object which was returned by value
from the function.

The code in Snippet (1) works on exactly the same principle as the following
code snippet:

Snippet (3):
------------

int main()
{
int const &r = 5;
}

In Snippet (1) and in Snippet (3), the const reference remains valid NOT
because a temporary has had its liftime extended, but because the reference
was initialised with an R-value. The C++ Standard defines this process:

If the initializer expression is an rvalue, with T2 a class type, and “cv1
T1” is reference-compatible with “cv2 T2,” the reference is bound in one of
the following ways (the choice is implementation-defined):
— The reference is bound to the object represented by the rvalue (see 3.10)
or to a sub-object within that object.
— A temporary of type “cv1 T2” [sic] is created, and a constructor is called
to copy the entire rvalue object into the temporary. The reference is bound
to the temporary or to a sub-object within the temporary.

This explains why the following code snippet fails to compile, and disproves
that the lifetime of a temporary is extended by binding a const reference to
it.

Snippet (4):
------------

class MyClass {
private:
MyClass(MyClass const &); /* Can't copy-construct! */

public:
MyClass() {}
};

int main()
{
MyClass const &r = MyClass();
}
 
V

Victor Bazarov

Frederick said:
There is a common misconception, (one which I myself also held at one
point), that a const reference can "extend the lifetime of a
temporary". Examples such as the following are given:

Snippet (1)
-----------

#include <string>
using std::string;

#include <iostream>
using std::cout;

string Func()
{
return "Hello";
}

int main()
{
string const &str = Func();

cout << str << '\n';
}

In the above snippet, no temporary "has had its lifetime extended".
If such a thing were true, then the following code would be perfectly
OK:

Snippet (2):
------------

#include <string>
using std::string;

string Func()
{
return "Hello";
}

int main()
{
string const &cstr = Func();

string &str = const_cast<string&>(cstr);

str = "World";
}


But alas, Snippet (2) exhibits undefined behaviour, because the
reference does not refer to the original non-const object which was
returned by value from the function.

The code in Snippet (1) works on exactly the same principle as the
following code snippet:

Snippet (3):
------------

int main()
{
int const &r = 5;
}

In Snippet (1) and in Snippet (3), the const reference remains valid
NOT because a temporary has had its liftime extended, but because the
reference was initialised with an R-value. The C++ Standard defines
this process:

If the initializer expression is an rvalue, with T2 a class type, and
“cv1 T1” is reference-compatible with “cv2 T2,” the reference is
bound in one of the following ways (the choice is
implementation-defined):
— The reference is bound to the object represented by the rvalue (see
3.10) or to a sub-object within that object.
— A temporary of type “cv1 T2” [sic] is created, and a constructor is
called to copy the entire rvalue object into the temporary. The
reference is bound to the temporary or to a sub-object within the
temporary.

This explains why the following code snippet fails to compile, and
disproves that the lifetime of a temporary is extended by binding a
const reference to it.

Snippet (4):
------------

class MyClass {
private:
MyClass(MyClass const &); /* Can't copy-construct! */

public:
MyClass() {}
};

int main()
{
MyClass const &r = MyClass();
}

Here is another interesting example:

char const* hello() { return "Hello"; }

#include <string>
#include <iostream>

int main() {
std::string const& str = hello();
std::cout << str << std::endl;
}

The temporary is not returned from anywhere, it's created as the result
of the initialisation of the reference. There is no lifetime "extention",
only the lifetime of the temporary to which the reference is bound. If
it is the original temporary, you can think of it as "extention" of its
lifetime, but the point you made with your private copy-c-tor constructor
is that it doesn't have to be the original temporary. It could just as
well be *another* temporary, copy-constructed from the "original" or from
something else, like in my example.

Just like in any other situation with returning by value, the copy-c-tor
has to be available for the code to be legal, even if copying is not done.

V
 
O

Old Wolf

Frederick said:
There is a common misconception, (one which I myself also held at one point),
that a const reference can "extend the lifetime of a temporary".

That is not a misconception.
#include <string>
using std::string;

#include <iostream>
using std::cout;

string Func()
{
return "Hello";
}

int main()
{
string const &str = Func();

cout << str << '\n';
}

In the above snippet, no temporary "has had its lifetime extended".

The return value of Func() is a temporary. It has its lifetime extended
by being bound to 'str'.

If it did not have its lifetime extended, then the cout << str would be
referencing an object that no longer existed.
If such a thing were true, then the following code would be perfectly OK:

Snippet (2):
------------

#include <string>
using std::string;

string Func()
{
return "Hello";
}

int main()
{
string const &cstr = Func();

string &str = const_cast<string&>(cstr);

str = "World";
}

yes, that code is OK.
But alas, Snippet (2) exhibits undefined behaviour,

No it doesn't.
because the reference does not refer to the original non-const object
which was returned by value from the function.

The reference refers to the return value of the function, not the
original object that was returned by value, as you say.
The code in Snippet (1) works on exactly the same principle as the following
code snippet:

Snippet (3):
------------

int main()
{
int const &r = 5;
}

In Snippet (1) and in Snippet (3), the const reference remains valid NOT
because a temporary has had its liftime extended,

That code creates a temporary int , initializes it to 5, and then
binds 'r' to it. The temporary has its lifetime extended to the
scope of 'r'.
but because the reference was initialised with an R-value.
The C++ Standard defines this process:

- A temporary of type "cv1 T2" [sic] is created, and a constructor is called
to copy the entire rvalue object into the temporary. The reference is bound
to the temporary or to a sub-object within the temporary.

Yes, that is what happened. A temporary of type "int" was created,
and its "copy constructor" was used to initialize it with 5. The
reference
is then bound to the temporary, and that temporary's lifetime is
extended.

I put "copy constructor" in quotes because ints don't really have copy
constructors. The string example is more straight forward.
This explains why the following code snippet fails to compile
and disproves that the lifetime of a temporary is extended by binding
a const reference to it.

It does no such thing.
Snippet (4):
------------

class MyClass {
private:
MyClass(MyClass const &); /* Can't copy-construct! */

public:
MyClass() {}
};

int main()
{
MyClass const &r = MyClass();
}

This fails to compile because the standard explicitly says that a
copy constructor is needed when initializing a reference with
a temporary. In fact you quoted that section of the standard earlier.

In this whole thread, I think you are confusing the object being
returned, with the return value. The return value (a temporary)
exists in the scope of the calling function, and is copy-constructed
from the object being returned (which exists in the scope of the
called function).

Normally the return value is destroyed at the end of the full-
expression, since it is a temporary. But when it is bound to
a const reference, it is not destroyed.
 
F

Frederick Gotham

Old Wolf posted:
No it doesn't.


Read the rules about binding a const reference to an R-value:

- A temporary of type "cv1 T2" [sic] is created, and a constructor is called
to copy the entire rvalue object into the temporary. The reference is bound
to the temporary or to a sub-object within the temporary.

Take note of the fifth word in the above paragraph -- "cv1".

Code Snippet (2) invokes Undefined Behaviour.
 
O

Old Wolf

Frederick said:
Old Wolf posted:
No it doesn't.

Read the rules about binding a const reference to an R-value:

- A temporary of type "cv1 T2" [sic] is created, and a constructor is called
to copy the entire rvalue object into the temporary. The reference is bound
to the temporary or to a sub-object within the temporary.

Incidentally, this quote disproves your entire position: it says
clearly that a temporary is created, and the reference is bound to it.

The temporary continues to exist after the end of the declaration,
ie. it has its lifetime extended.
Take note of the fifth word in the above paragraph -- "cv1".

What about it?
Code Snippet (2) invokes Undefined Behaviour.

It doesn't. Here's the code again:

string const &cstr = Func();
string &str = const_cast<string&>(cstr);

The initialization of 'str' is covered by this part of 8.5.3#5:

A reference to type "cv1 T1" is initialized by an expression of
type "cv2 T2" as follows:
If the initializer expression is an lvalue (but is not a bitfield),
and "cv1 T1" is reference-compatible with "cv2 T2," or
[other case snipped], then the reference is bound directly
to the initializer expression lvalue

The initializer expression is "const_cast<string &>(cstr)", which
is an lvalue (*). cv1 and cv2 are both nothing, T1 is
"string" and T2 is "string&", so "cv1 T1" and "cv2 T2" are
reference-compatible (which is explained in 8.5.3).

So this section seems to covers the initialization.

Can you find any compiler that can't handle Snippet 2? gcc has
no problem with it.


(*) 5.2.11:
The result of the expression const_cast<T>(v) is of type T.
If T is a reference type, the result is an lvalue;
 
N

Noah Roberts

Old said:
string const &cstr = Func();
string &str = const_cast<string&>(cstr);

Now, at this point is modifying the object through str defined or no?
 
K

Kai-Uwe Bux

I am afraid it does. I don't like it, but I can see the point.

Read the rules about binding a const reference to an R-value:

- A temporary of type "cv1 T2" [sic] is created, and a constructor is
called to copy the entire rvalue object into the temporary. The reference
is bound to the temporary or to a sub-object within the temporary.

Incidentally, this quote disproves your entire position: it says
clearly that a temporary is created, and the reference is bound to it.

The temporary continues to exist after the end of the declaration,
ie. it has its lifetime extended.
Take note of the fifth word in the above paragraph -- "cv1".

What about it?

We shall see below.
It doesn't. Here's the code again:

string const &cstr = Func();
string &str = const_cast<string&>(cstr);

The initialization of 'str' is covered by this part of 8.5.3#5:

But the important part is the initialization of cstr: The compiler is free
to initialize it as follows:

a) a temporary X of type const string (this is where the cv1 kicks in!) is
created and initialized from the return value of Func().

b) this temporary (of type const string) is bound to cstr (and if you
insist, has it's lifetime extended).


Now, when you do the const_cast, you are casting away the constness of X
which has type const string. Any attempt to modify the value will then be
undefined behavior.
A reference to type "cv1 T1" is initialized by an expression of
type "cv2 T2" as follows:
If the initializer expression is an lvalue (but is not a bitfield),
and "cv1 T1" is reference-compatible with "cv2 T2," or
[other case snipped], then the reference is bound directly
to the initializer expression lvalue

The initializer expression is "const_cast<string &>(cstr)", which
is an lvalue (*). cv1 and cv2 are both nothing, T1 is
"string" and T2 is "string&", so "cv1 T1" and "cv2 T2" are
reference-compatible (which is explained in 8.5.3).

So this section seems to covers the initialization.

Can you find any compiler that can't handle Snippet 2? gcc has
no problem with it.

Nope, and I think it is a defect in the standard.
(*) 5.2.11:
The result of the expression const_cast<T>(v) is of type T.
If T is a reference type, the result is an lvalue;



Best

Kai-Uwe Bux
 
O

Old Wolf

Kai-Uwe Bux said:
Old said:
Frederick said:
- A temporary of type "cv1 T2" [sic] is created, and a constructor is
called to copy the entire rvalue object into the temporary. The reference
is bound to the temporary or to a sub-object within the temporary.
Here's the code again:

string const &cstr = Func();
string &str = const_cast<string&>(cstr);

But the important part is the initialization of cstr: The compiler is free
to initialize it as follows:

a) a temporary X of type const string (this is where the cv1 kicks in!) is
created and initialized from the return value of Func().

b) this temporary (of type const string) is bound to cstr

Now, when you do the const_cast, you are casting away the constness of X
which has type const string. Any attempt to modify the value will then be
undefined behavior.

Good explanation.
Nope, and I think it is a defect in the standard.

Well, the standard says that the compiler has two options:
1) Bind the reference to the (non-const) return value
2) Bind the reference to a "cv1 T2" temporary constructed
from the return value.

So all these compilers that work, could simply be choosing option 1.

But it would seem to make more sense if the constructed temporary
had type "cv2 T2". Why does the standard say "[sic]" in it?

Also interesting is the earlier part of the same section that says that
if "cv2 T2" can be implicitly converted to an lvalue of type "cv3 t3",
then this conversion must be performed and bound to the reference:

A reference to type "cv1 T1" is initialized by an expression of
type "cv2 T2" as follows: If the initializer expression
- has a class type (i.e., T2 is a class type) and can be
implicitly converted to an lvalue of type "cv3 T3," where
"cv1 T1" is reference-compatible with "cv3 T3" 92)
(this conversion is selected by enumerating the
applicable conversion functions (13.3.1.6) and choosing
the best one through overload resolution (13.3)),

then the reference is bound directly to ... the lvalue result of the
conversion in the second case.

If we choose "cv3 t3" = "std::string", does that fit the criteria? A
std::string rvalue can be converted to a std::string lvalue by using
the copy-constructor!
 
A

Andrey Tarasevich

Frederick said:
There is a common misconception, (one which I myself also held at one point),
that a const reference can "extend the lifetime of a temporary". Examples
such as the following are given:

Snippet (1)
-----------
...
string Func()
{
return "Hello";
}

int main()
{
string const &str = Func();

cout << str << '\n';
}

In the above snippet, no temporary "has had its lifetime extended".

That's not correct. There's no definitive answer to what exactly happens there,
because it depends on implementation-defined behavior. The implementation is
free to choose one of two ways to initialize the reference: either 1) bind it
directly to the temporary object returned by 'Func', thus extending its
lifetime, or 2) create another temporary object of type 'const string' (by
copying the original temporary) and bind the reference to it, thus, again,
extending its lifetime. (The implementation is also free to apply the second
method repeatedly, until it eventually chooses the first method.)

Regardless of the method chosen by implementation, we have the lifetime of some
temporary extended here.
If such a
thing were true, then the following code would be perfectly OK:

Snippet (2):
------------
...
string Func()
{
return "Hello";
}

int main()
{
string const &cstr = Func();

string &str = const_cast<string&>(cstr);

str = "World";
}


But alas, Snippet (2) exhibits undefined behaviour, because the reference
does not refer to the original non-const object which was returned by value
from the function.

Incorrect. _In_ _general_ _case_, the reference indeed does not refer to the
original object. _In_ _general_ _case_ it is possible that the reference is
actually bound to a 'const string' object, which means that the code indeed
produces undefined behavior _in_ _general_ _case_.

However, if a concrete implementation defines its behavior so that the reference
is always bound to the original object returned by 'Func', then the code is
perfectly fine within the bounds of that implementation.
The code in Snippet (1) works on exactly the same principle as the following
code snippet:

Snippet (3):
------------

int main()
{
int const &r = 5;
}

It _might_ work on exactly the same principle. The standard separated the cases
of class types and non-class types in 8.5.3/5 for a reason: there are
differences between reference initialization rules in those cases.
In Snippet (1) and in Snippet (3), the const reference remains valid NOT
because a temporary has had its liftime extended, but because the reference
was initialised with an R-value.

Huh? Initializing a reference with an rvalue always results in some temporary
having its lifetime extended. How can you contrapose these two concepts?

The C++ Standard defines this process:
If the initializer expression is an rvalue, with T2 a class type, and “cv1
T1” is reference-compatible with “cv2 T2,” the reference is bound in one of
the following ways (the choice is implementation-defined):
— The reference is bound to the object represented by the rvalue (see 3.10)
or to a sub-object within that object.
— A temporary of type “cv1 T2” [sic] is created, and a constructor is called
to copy the entire rvalue object into the temporary. The reference is bound
to the temporary or to a sub-object within the temporary.

This explains why the following code snippet fails to compile, and disproves
that the lifetime of a temporary is extended by binding a const reference to
it.

Snippet (4):
------------

class MyClass {
private:
MyClass(MyClass const &); /* Can't copy-construct! */

public:
MyClass() {}
};

int main()
{
MyClass const &r = MyClass();
}

This doesn't disprove anything. In all "snippets" you provided so far some
temporary always has its lifetime extended. It might be completely different
temporary, not exactly the one you originally expected, but there;'s always one
that gets an extension.
 
A

Andrey Tarasevich

Old said:
...
Also interesting is the earlier part of the same section that says that
if "cv2 T2" can be implicitly converted to an lvalue of type "cv3 t3",
then this conversion must be performed and bound to the reference:

A reference to type "cv1 T1" is initialized by an expression of
type "cv2 T2" as follows: If the initializer expression
- has a class type (i.e., T2 is a class type) and can be
implicitly converted to an lvalue of type "cv3 T3," where
"cv1 T1" is reference-compatible with "cv3 T3" 92)
(this conversion is selected by enumerating the
applicable conversion functions (13.3.1.6) and choosing
the best one through overload resolution (13.3)),

then the reference is bound directly to ... the lvalue result of the
conversion in the second case.

If we choose "cv3 t3" = "std::string", does that fit the criteria? A
std::string rvalue can be converted to a std::string lvalue by using
the copy-constructor!

I don't see how you can perform such a conversion using a copy constructor.
Remember that temporary objects are not lvalues (as seen from "outside"). No,
the only way to convert an rvalue of class type to an lvalue is a user-defined
conversion operator returning a reference (which is mentioned in footnote 92).
 
F

Frederick Gotham

Andrey Tarasevich posted:
That's not correct. There's no definitive answer to what exactly happens
there, because it depends on implementation-defined behavior.

(Apologies for the punctuation in the first sentence)

Would it be fair to say, that when, a const reference is bound to an R-value,
e.g.:

{
Type const &r = FuncReturnByValue();
}

That it's as if you had written:

{
Type const obj( FuncReturnByValue() );

Type const &r = obj;
}
 
V

Victor Bazarov

Frederick said:
Andrey Tarasevich posted:


(Apologies for the punctuation in the first sentence)

Would it be fair to say, that when, a const reference is bound to an
R-value, e.g.:

{
Type const &r = FuncReturnByValue();
}

That it's as if you had written:

{
Type const obj( FuncReturnByValue() );

Type const &r = obj;
}

It could be that 'obj' is const, or it could be that 'obj' is not const.
And then again, during the construction of 'obj' there is probably a temp
object somewhere created, and a reference is bound to it, and so you get
the chicken and egg problem: you can't express reference binding to a temp
without resorting to reference binding to some other potential temp.

V
 
A

Andrey Tarasevich

Frederick said:
...
Would it be fair to say, that when, a const reference is bound to an R-value,
e.g.:

{
Type const &r = FuncReturnByValue();
}

That it's as if you had written:

{
Type const obj( FuncReturnByValue() );

Type const &r = obj;
}

No and yes.

"No" because the implementation is free to choose to follow the above logic
(i.e. create an extra 'const' temporary), or decide to bind the reference
directly to the original temporary returned by the function. The original
temporary is not const (assuming the return value of 'FuncReturnByValue' is not
const).

"Yes" because in the portable code we have to assume that the most restrictive
behavior takes place: an extra const temporary is created.
 
F

Frederick Gotham

Andrey Tarasevich posted:
No, the only way to convert an rvalue of class type to
an lvalue is a user-defined conversion operator returning a reference
(which is mentioned in footnote 92).


Perhaps something like:

template<class T>
struct Temp {
T obj;

T &operator&()
{
return obj;
}
};

struct MyType {};

void TakeRef(MyType&) {}

int main()
{
TakeRef( &Temp<MyType>() );
}
 
F

Frederick Gotham

Frederick Gotham posted:
template<class T>
struct Temp {
T obj;

T &operator&()
{
return obj;
}
};

struct MyType {};

void TakeRef(MyType&) {}

int main()
{
TakeRef( &Temp<MyType>() );
}


Bad example!

I had intended to overload the "address of" operator in order to return the
address of the "temporary" object.

(I realise that using it to return a reference is an abuse of operator
overloading!)
 

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

Members online

No members online now.

Forum statistics

Threads
473,770
Messages
2,569,584
Members
45,075
Latest member
MakersCBDBloodSupport

Latest Threads

Top