【问题标题】:Move semantics in derived-to-base class conversions在派生类到基类的转换中移动语义
【发布时间】:2019-11-23 11:41:34
【问题描述】:

考虑以下类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. return 语句中的表达式buffer 是一个左值。由于它是一个不再使用的本地对象,编译器将其转换为一个右值
  2. buffer 对象的类型为 BufferBuilderBufferBufferBuilder 的公共基类,所以这个BufferBuilder 对象被隐式转换为Buffer 对象。
  3. 反过来,这种转换意味着隐式引用到派生到引用到基础的转换(即,对BufferBuilder 的引用到对Buffer 的引用)。对BufferBuilder 的引用是一个右值引用(参见1.),它变成了对Buffer 的右值引用。
  4. Buffer 的右值引用匹配Buffer 的移动构造函数,用于构造make_zeroed_buffer() 按值返回的Buffer 对象。因此,返回值是通过从对象bufferBuffer 部分移动来构造的。

【问题讨论】:

  • 如果没有复制省略发生 是什么让你认为这个条件永远会得到满足?据我回忆,当你明确地return std::move(buffer); 时,编译器(clang)会发出一个关于抑制复制省略的警告。
  • @Walter 是什么让您认为复制省略将永远得到满足?
  • 如果我没记错的话,在这个非常特殊的情况下你必须使用return std::move(buffer)
  • 无复制省略。违反下一条语句:在返回语句中,当操作数是与函数返回类型相同的类类型(忽略 cv 限定)的纯右值时
  • 缓冲区对象的类型为 BufferBuilder。 Buffer是BufferBuilder的公共基类,所以这个BufferBuilder对象被隐式转换为Buffer对象。不正确。 BufferBuilder 对象被隐式复制到 Builder 对象。您对派生类的指针感到困惑。

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


【解决方案1】:

RVO 优化

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

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

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

在以下情况下是允许的[...]:

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

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

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

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

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


隐式移动

现在,隐式移动呢?

上面的缓冲区对象是否保证被移出???

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

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

简而言之,这是因为重载解析失败。 它试图匹配移动构造函数(Buffer::Buffer(Buffer&amp;&amp;)),而你有一个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

【讨论】:

  • 在标准语言中,隐式移动是不允许的,因为“所选构造函数的第一个参数的类型 (Buffer&amp;&amp;) 不是对对象类型 (BufferBuilder&amp;&amp;) 的右值引用”@987654325 @
  • 是的,这就是我写的(换句话说)。不过谢谢,也许我可以改写更好,并添加对标准的引用。
  • 你说得很好,我只是想展示“证据”。
  • 如果我对规范的阅读是正确的,本地类的这种修改应该会让编译器移动它:struct BufferBuilder { BufferBuilder():buffer(std::byte{0}) {} Buffer buffer; operator Buffer() &amp;&amp; { return (Buffer&amp;&amp;)buffer; } }; 因为不再有“选定的构造函数”了。 (编辑:在 Buffer 中需要一个公共构造函数:/)
  • GCC 是否有一个等同于-Wmove 的标志?
【解决方案2】:

make_zeroed_buffer() 中的对象缓冲区将在 Buffers 的复制构造函数用于返回值的帮助下复制后销毁。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-01-08
    • 2013-11-17
    • 1970-01-01
    • 2021-01-15
    • 1970-01-01
    相关资源
    最近更新 更多