【问题标题】:C++ Why does returning rvalue reference change caller's behavior when function signature does not return rvalue reference?C++ 为什么当函数签名不返回右值引用时,返回右值引用会改变调用者的行为?
【发布时间】:2021-03-30 22:29:35
【问题描述】:

我遇到了一些关于右值返回的行为,我无法理解。 假设我们有以下结构:

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);
   }
};

如下使用:

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());
}

现在我明白为什么Bar MoveConstructedExplicit(myFoo.GetBarRValueExplicit()) 调用移动构造函数了。 但由于函数Foo::GetBarRValue() 没有显式返回Bar&amp;&amp;,我希望它的调用给出与Foo::GetBar() 相同的行为。我不明白在这种情况下为什么/如何调用移动构造函数。据我所知,没有办法知道GetBarRValue() 的实现将myBar 转换为rValue 引用。

我的编译器是否在对我进行优化(在 Visual Studio 的调试版本中对此进行测试,显然无法禁用返回值优化)? 我觉得有点令人沮丧的是,调用方的行为可能会受到GetBarRValue() 的实现的影响。 GetBarRValue() 签名中的任何内容都没有告诉我们,如果调用两次,它将给出未定义的行为。在我看来,正因为如此,当函数没有显式返回 && 时,return std::move(x) 是不好的做法。

有人可以向我解释这里发生了什么吗?谢谢!

【问题讨论】:

  • FWIW,如果编译器可以看到定义(即,它在同一个源文件中,或者在头文件中),那么编译器可以知道它做了一个std::move
  • 一开始我还以为是内联,但即使在单独的 .h 和 .cpp 文件中声明和定义,行为也是一样的。

标签: c++ move-semantics rvalue copy-elision


【解决方案1】:

发生的事情是你在那里看到了省略。您正在使用Bar 的简单类型在return std::move(x) 上移动构造;那么编译器正在删除副本。

可以看到GetBarRValuehere的非优化汇编。对移动构造函数的调用实际上发生在 GetBarRValue 函数中,而不是在返回时发生。回到main,它只是做了一个简单的lea,根本没有调用任何构造函数。

【讨论】:

  • 这是有道理的,它也解释了为什么输出没有我预期的那么冗长。谢谢!
  • 通过在您共享的 Godbolt 链接中使用 -fno-elide-constructors 编译器选项,我得到了我期望的输出。这证实了这一点。
【解决方案2】:

关键是

   Bar myBar;

Foo 的数据成员。因此,对于Foo的每个成员函数,它的生存时间都比他们的长。换句话说,这些函数中的每一个都返回一个值或对范围大于函数范围的值的引用。

现在,

Bar GetBar()
   {
      return myBar;
   }

编译器可以“看到”您返回的值将在函数完成后生效。函数必须“按值”返回它的值,而且由于它的参数肯定不是临时的,编译器会选择复制构造函数。

如果你像这样尝试这个功能:

Bar GetBar()
   {
      Bar myBar; // shadows this->myBar
      return myBar;
   }

编译器应该注意到返回值的范围即将到期,因此它将其“种类”从左值更改为右值并使用移动构造函数(或复制省略,但这是另一回事)。

第二个功能:

   Bar GetBarRValue()
   {
      return std::move(myBar);
   }
 

这里编译器可以“看到”与以前相同的返回值:该值必须“按值”传递。但是,程序员已将myBar 的“种类”从左值更改为x 值(可寻址的对象,但可以视为临时对象)。这意味着:“嘿,编译器,myBar 的状态不再需要保护,你可以窃取它的内容”。编译器会乖乖的选择move构造函数。因为你,程序员,让“他”这样做。

第三种情况,

   Bar&& GetBarRValueExplicit()
   {
      return std::move(myBar);
   }

编译器不会进行任何转换,也不会调用构造函数。只会返回类型为“r 值引用”的引用(“变相的指针”)。然后,该值将用于初始化对象MoveConstructed,这是将根据其参数类型调用移动构造函数的位置。

【讨论】:

  • 这个答案不准确。我观察到的是复制省略,它适用于翻译单元。仅当我们认为代码位于同一翻译单元中时,您对编译器“看到”函数范围内的解释才有效。当然,我没有指定它,但我在一个更复杂的设置中使用多个翻译单元对此进行了测试。还有什么你错了:Bar GetBar() { Bar myBar; // shadows this-&gt;myBar return myBar; } 两种实现都会在调用方调用移动构造函数,但在这两种情况下都被忽略了。
  • @UncleBen 你明确地问:“调用者如何知道在一种情况下返回的值可以安全移动,但在另一种情况下则不能?”据我了解,这个问题与复制省略无关,而是与复制和移动构造函数之间的选择有关。顺便说一句,Bar CopyConstructed(myFoo.GetBar()); 必须使用复制/移动省略,因为对象初始化中涉及的函数返回一个 pr 值。如果您询问输出的冗长程度出乎意料地小,我的回答会完全不同。
  • 如果您想了解为什么调用者和被调用者在相同或不同的编译单元中并不重要,请观看:C++Now 2018: Jon Kalb “Copy Elision”
  • 谢谢,我一定会的! :) 问题是,我错了,来电者不知道在一种情况下移动是安全的,但在另一种情况下不知道。当我们复制/移动返回值时,我得到的输出是从函数范围内生成的。在调用者的作用域中,我们总是试图移动 GetBar() 返回的右值,但移动被忽略了。
猜你喜欢
  • 1970-01-01
  • 2014-08-28
  • 2020-09-21
  • 1970-01-01
  • 1970-01-01
  • 2012-02-08
  • 2022-01-16
  • 1970-01-01
  • 2014-03-29
相关资源
最近更新 更多