Reputation: 82
I've come across some behavior I cannot wrap my head around regarding rvalue return. Let's say we have the following structs:
struct Bar
{
int a;
Bar()
: a(1)
{
std::cout << "Default Constructed" << std::endl;
}
Bar(const Bar& Other)
: a(Other.a)
{
std::cout << "Copy Constructed" << std::endl;
}
Bar(Bar&& Other)
: a(Other.a)
{
std::cout << "Move Constructed" << std::endl;
}
~Bar()
{
std::cout << "Destructed" << std::endl;
}
Bar& operator=(const Bar& Other)
{
a = Other.a;
std::cout << "Copy Assigment" << std::endl;
return *this;
}
Bar& operator=(Bar&& Other) noexcept
{
a = Other.a;
std::cout << "Move Assigment" << std::endl;
return *this;
}
};
struct Foo
{
Bar myBar;
Bar GetBar()
{
return myBar;
}
// Note that we are not returning Bar&&
Bar GetBarRValue()
{
return std::move(myBar);
}
Bar&& GetBarRValueExplicit()
{
return std::move(myBar);
}
};
Being used as followed:
int main()
{
Foo myFoo;
// Output:
// Copy Constructed
Bar CopyConstructed(myFoo.GetBar());
// Output:
// Move Constructed
Bar MoveConstructedExplicit(myFoo.GetBarRValueExplicit());
// Output:
// Move Constructed
//
// I don't get it, GetBarRValue() has has the same return type as GetBar() in the function signature.
// How can the caller know in one case the returned value is safe to move but not in the other?
Bar MoveConstructed(myFoo.GetBarRValue());
}
Now I get why Bar MoveConstructedExplicit(myFoo.GetBarRValueExplicit())
calls the move constructor.
But since the function Foo::GetBarRValue()
does not explicitly returns a Bar&&
I expected its call to give the same behavior as Foo::GetBar()
. I don't understand why/how the move constructor is called in that case. As far as I know, there is no way to know that the implementation of GetBarRValue()
casts myBar
to an rValue reference.
Is my compiler playing optimization tricks on me (testing this in debug build in Visual Studio, apparently return value optimizations cannot be disabled)?
What I find slightly distressing is the fact that the behavior on the caller's side can be influenced by the implementation of GetBarRValue()
. Nothing in the GetBarRValue()
signature tells us it will give undefined behavior if called twice. Seems to me because of this it's bad practice to return std::move(x)
when the function does not explicitly returns a &&.
Can someone explain to me what is happening here? Thanks!
Upvotes: 0
Views: 240
Reputation: 2861
The key point is that
Bar myBar;
is a Foo
's data member. Therefore, to each of Foo
's member function, its time of living is longer than theirs. In other words, each of these functions returns a value or a reference to a value whose scope is larger than that of the function.
Now,
Bar GetBar()
{
return myBar;
}
The compiler can "see" that you return a value that will live after the function has finished. The function must return its value "by value", and since its argument is certainly not a temporary, the compiler will chose the copy constructor.
If you experimented with this function like this:
Bar GetBar()
{
Bar myBar; // shadows this->myBar
return myBar;
}
the compiler should notice that the scope of the return value is expiring, so it would change its "kind" from l-value to r-value and use a move constructor (or copy elision, but it's a different story).
The second function:
Bar GetBarRValue()
{
return std::move(myBar);
}
Here the compiler can "see" the same return value as before: the value must be passed "by value". However, the programmer has changed the "kind" of myBar
from l-value to x-value (object that is addressable, but can be treated as a temporary). This means: "Hey, compiler, the state of myBar
needs no longer be protected, you can steal its contents". The compiler will obediently chose the move constructor. Because you, the programmer, let "him" do so.
In the third case,
Bar&& GetBarRValueExplicit()
{
return std::move(myBar);
}
The compiler will do no conversion, no constructor will be invoked. Just a reference (a "pointer in disguise") of kind "r-value reference" will be returned. Then, this value will be used to initialize an object, MoveConstructed
, and this is where the move constructor will be invoked, based on the type of its argument.
Upvotes: 0
Reputation: 10022
What's happening is you are seeing elision there. You are move-constructing on return std::move(x)
with a simple type of Bar
; then the compiler is eliding the copy.
You can see the non-optimized assembly of GetBarRValue
here. The call to the move constructor is actually happening in the GetBarRValue
function, not upon returning. Back in main
, it's just doing a simple lea
, it's not at all calling any constructor.
Upvotes: 2