简体   繁体   中英

C++ Why does returning rvalue reference change caller's behavior when function signature does not return rvalue reference?

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!

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM