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

时间:2019-07-14 14:44:14

标签: c++ c++17 move-semantics derived-class object-slicing

请考虑以下类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()。类BufferBuilder local class ,它是从Buffer公开派生的。其目的是创建Buffer对象。

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

   BufferBuilder buffer;

   // ...

   return buffer;
}

如果没有进行复制省略,是否保证上方的buffer对象会被移走?

我的推理如下:

  1. buffer语句中的表达式return lvalue 。由于它是不再使用的本地对象,因此编译器将其转换为 rvalue
  2. buffer对象的类型为BufferBuilderBufferBufferBuilder的公共基类,因此此BufferBuilder对象被隐式转换为Buffer对象。
  3. 此转换又暗含了对引用的隐式引用到对基准的引用转换(即,对BufferBuilder的引用到对Buffer的引用)。对BufferBuilder的引用是一个右值引用(请参阅1.),它变成对Buffer的右值引用。
  4. Buffer的右值引用与Buffer的move构造函数匹配,该构造函数用于构造Buffer按值返回的make_zeroed_buffer()对象。结果,通过从对象Buffer的{​​{1}}部分开始构造返回值。

2 个答案:

答案 0 :(得分:7)

RVO优化

  

如果没有复制省略[...]

实际上,不会出现复制省略(没有 if 的情况下)。

根据C ++标准class.copy.elision#1

  在以下情况下允许使用

[...]:

     

-在具有类返回类型的函数中的return语句中,当表达式是具有相同类型的非易失性自动对象([...])的名称时忽略cv限定)作为函数返回类型[...]

从技术上讲,当您返回派生的类并且进行切片操作时,将无法应用RVO。

从技术上讲,RVO会在堆栈帧的返回空间上构造本地对象。

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

通常,派生类与其父类可以具有不同的内存布局(大小,对齐方式等)。因此,不能保证可以在为返回的对象( parent )保留的位置中构造本地对象( derived )。


隐式举动

现在,隐式举动怎么办?

  

是否可以保证将上面的缓冲对象移出??

简而言之:不。相反,可以保证对象将被复制!

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

简而言之,发生这种情况是因为过载解析失败。 它尝试与移动构造函数(Buffer::Buffer(Buffer&&))匹配,而您有BufferBuild对象)。因此,它依赖于复制构造函数。

根据C ++标准class.copy.elision#3

  

[...]如果所选构造函数的第一个参数的类型或return_value重载不是对该对象类型的右值引用(可能是cv限定的),则再次执行重载解析,将对象视为左值。

因此,由于第一个重载解析失败(如上所述),因此该表达式将被视为 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直接建议您在返回表达式上使用std::move

答案 1 :(得分:-1)

make_zeroed_buffer()中的对象缓冲区将在销毁之后被销毁,并借助Buffers的副本构造函数获取返回值。