Returning by value [vs] returning by rvalue

Look at this code

1
2
3
4
5
6
7
8
9
10
11
string&& join(string&& rv, const char* s)
{
   return move(rv.append(s));
}

string temp()
{
   return "temp";
}

const string& r = join(temp(), "orary");


The source of this code is https://channel9.msdn.com/Events/GoingNative/2013/Don-t-Help-the-Compiler at minute 45:00

The guy says there's a problem with this code: r refers to a destroyed temporary.

To fix the problem, he says that we need to return by value instead of by rvalue reference

1
2
3
4
string join(string&& rv, const char* s)
{
   return move(rv.append(s));
}


What I don't understand is: why does r refer to a destroyed temporary?

We all know that a const reference bind to a temporary will extend its lifetime.

The guy explains that this rule is only valid when we bind the const reference DIRECTLY to the temporary, but since there's an interfering function (in this case, temp) this is not valide anymore.

I don't know if I understood correctly... but I still have some doubts...

Question number 2: how does returning by value actually solve the problem?

Question number 3: why is he returning using the move() function everytime? what happens if we omit that move()?

Thanks in advance.

Last edited on
What's wrong with this
const string& r = join(temp(), "orary");

Inside join(), the parameter rv binds the result of temp(). rv lives to the end of the function - it's a local name. The referent dies along with it.

There are three contexts in which temporaries are destroyed at a different point than the end of the full-expression [...] the third 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 of a subobject to which the reference is bound persists for the lifetime of the reference except [...] a temporary object bound to a reference parameter in a function call persists until the completion of the full-expression containing the call.

See points 5, 6.1:
http://eel.is/c++draft/class.temporary#5

How does returning by value actually solve the problem?

The referent is still alive at the the return statement. Instead of trying to spit out a reference to a object which is about to die, the result is moved into the result.

Why is Lavajev calling std::move()

Because return-value-optimization (or RVO) can't be applied because the type of the expression in the return statement does not match the return type of the function. Note that std::string::append() is not a std::string, it's a std::string&.

The use of move() causes a move into the function's return value. If it wasn't there it would be copied instead. This is because the append call is an lvalue, and the compiler won't move from lvalues.
Last edited on
Thank you, I understood almost everything except the question How does returning by value actually solve the problem?

Could you try to explain that in a different way?
I'll try!

The problem is fundamentally the lifetime of the referent.

Consider the incorrect function
int& f() { int x = 42; return x; }
Which always returns a dangling lvalue reference to x. No matter the context of the call, x is destroyed at the closing brace of f. Maybe this isn't apparent, but this is true even in a context like
int const& foo = f()
Where we fail to extend the lifetime of x, because the result of the function is not x, but a reference to it. Merely initializing another reference (foo) from the result of the function doesn't affect the lifetime of x. Indeed, there is nothing you can do to prevent x from being destroyed at the closing brace.

Now consider the original:
const string& r = join(temp(), "orary"); join() doesn't exactly return a dangling reference: it returns a reference to the result of the expression temp(). Then we initialize r from the return value.

We saw before (i.e., in [class.temporary]) that the temporary returned by temp() lives only until the semicolon. This is bad, because r clearly can be used after the semicolon.

To fix it, we need to force r to bind a result that isn't about to die. We do that by creating a new object - the result of join(), and move-initializing it with temp:
1
2
3
4
5
string join(string&& rv, const char* s) {
  // initialize the result of the function (a std::string) from
  // the result of the expression move(rv.append(s));
  return move(rv.append(s)); 
}


vs.
1
2
3
4
5
6
string&& join(string&& rv, const char* s) {
  // return a reference to the result of the expression
  // move(rv.append(s));  
  // we don't know how long the referent bound to rv will live 
  return move(rv.append(s)); 
}
Last edited on
Thank you for the detailed explanation!

Let's see if I understood though...

When join returns a string&& we are basically taking the temporary returned by temp() and appending some other string to it.

At the end of the day, we have r which is referring to the same temporary, but modified by join()

At the end of the definition of r the temporary is destroyed, so r is basically becoming a dangling reference after the semicolon, and using it will be dangerous.

When join() returns by value, the temporary is moved into a newly created object... and that object's lifetime is then extended by r.

Am I correct?
Topic archived. No new replies allowed.