在派生到基础 class 转换中移动语义

Move semantics in derived-to-base class conversions

考虑以下 class Buffer,其中包含一个 std::vector 对象:

#include <vector>
#include <cstddef>

class Buffer {
   std::vector<std::byte> buf_;
protected:
   Buffer(std::byte val): buf_(1024, val) {}
};

现在,考虑下面的函数 make_zeroed_buffer()。 class BufferBuilderlocal class,public 仅派生自 Buffer。它的目的是创建 Buffer 个对象。

Buffer make_zeroed_buffer() {
   struct BufferBuilder: Buffer {
      BufferBuilder(): Buffer(std::byte{0}) {}
   };

   BufferBuilder buffer;

   // ...

   return buffer;
}

如果没有发生复制省略,上面的 buffer 对象是否保证被移动?

我的推理如下:

  1. return 语句中的表达式 buffer 是一个 左值 。由于它是一个不再使用的 local object,编译器将其转换为 rvalue.
  2. buffer 对象的类型为 BufferBuilderBufferBufferBuilder 的 public 基础 class,所以这个 BufferBuilder 对象被隐式转换为 Buffer 对象。
  3. 此转换反过来意味着隐式引用到派生到引用到基的转换(即,对 BufferBuilder 的引用到对 Buffer 的引用)。对 BufferBuilder 的引用是右值引用(见 1.),它变成对 Buffer.
  4. 的右值引用
  5. Buffer 的右值引用匹配 Buffer 的移动构造函数,该构造函数用于构造 make_zeroed_buffer() return 的 Buffer 对象价值。因此,return 值是通过从对象 buffer.
  6. Buffer 部分移动而构造的

来自 make_zeroed_buffer() 的对象缓冲区将在 return 值的缓冲区复制构造函数的帮助下进行复制后被销毁。

RVO 优化

If no copy elision takes place [...]

实际上,复制省略不会发生(没有if)。

来自 C++ 标准 class.copy.elision#1:

is permitted in the following circumstances [...]:

-- in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object ([...]) with the same type (ignoring cv-qualification) as the function return type [...]

从技术上讲,当您 return 派生 class 并且发生切片操作时,无法应用 RVO。

从技术上讲,RVO 在堆栈帧上 returning space 上构建本地对象。

|--------------|
| local vars   |
|--------------|
| return addr  |
|--------------|
| return obj   |
|--------------|

通常,派生的 class 可以具有与其父级不同的内存布局(不同的大小、对齐方式等)。所以不能保证本地对象(derived)可以在为returned对象(parent)保留的地方构造.


隐式着手

现在,隐式移动怎么样?

is the buffer object above guaranteed to be moved from???

简而言之:没有。反之则保证对象会被复制!

在这种特殊情况下,隐式移动将不会执行,因为切片

简而言之,发生这种情况是因为重载解析失败。 它试图匹配 move-constructor (Buffer::Buffer(Buffer&&)) 而你有一个 BufferBuild 对象)。所以它回退到复制构造函数。

来自 C++ 标准 class.copy.elision#3:

[...] if the type of the first parameter of the selected constructor or the return_­value overload is not an rvalue reference to the object's type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.

因此,由于第一次重载解析失败(正如我上面所说的),表达式将被视为 lvalue(而不是 rvalue), 抑制移动.

Arthur O'Dwyer 的一次有趣演讲专门提到了这个案例。 Youtube Video.


补充说明

在 clang 上,您可以传递标志 -Wmove 以检测此类问题。 确实 for your code:

local variable 'buffer' will be copied despite being returned by name [-Wreturn-std-move]

   return buffer;

          ^~~~~~

<source>:20:11: note: call 'std::move' explicitly to avoid copying

   return buffer;

clang 直接建议您在 return 表达式上使用 std::move