在派生到基础 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 BufferBuilder
是 local class,public 仅派生自 Buffer
。它的目的是创建 Buffer
个对象。
Buffer make_zeroed_buffer() {
struct BufferBuilder: Buffer {
BufferBuilder(): Buffer(std::byte{0}) {}
};
BufferBuilder buffer;
// ...
return buffer;
}
如果没有发生复制省略,上面的 buffer
对象是否保证被移动?
我的推理如下:
return
语句中的表达式 buffer
是一个 左值 。由于它是一个不再使用的 local object,编译器将其转换为 rvalue.
buffer
对象的类型为 BufferBuilder
。 Buffer
是 BufferBuilder
的 public 基础 class,所以这个 BufferBuilder
对象被隐式转换为 Buffer
对象。
- 此转换反过来意味着隐式引用到派生到引用到基的转换(即,对
BufferBuilder
的引用到对 Buffer
的引用)。对 BufferBuilder
的引用是右值引用(见 1.),它变成对 Buffer
. 的右值引用
- 对
Buffer
的右值引用匹配 Buffer
的移动构造函数,该构造函数用于构造 make_zeroed_buffer()
return 的 Buffer
对象价值。因此,return 值是通过从对象 buffer
. 的 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
。
考虑以下 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 BufferBuilder
是 local class,public 仅派生自 Buffer
。它的目的是创建 Buffer
个对象。
Buffer make_zeroed_buffer() {
struct BufferBuilder: Buffer {
BufferBuilder(): Buffer(std::byte{0}) {}
};
BufferBuilder buffer;
// ...
return buffer;
}
如果没有发生复制省略,上面的 buffer
对象是否保证被移动?
我的推理如下:
return
语句中的表达式buffer
是一个 左值 。由于它是一个不再使用的 local object,编译器将其转换为 rvalue.buffer
对象的类型为BufferBuilder
。Buffer
是BufferBuilder
的 public 基础 class,所以这个BufferBuilder
对象被隐式转换为Buffer
对象。- 此转换反过来意味着隐式引用到派生到引用到基的转换(即,对
BufferBuilder
的引用到对Buffer
的引用)。对BufferBuilder
的引用是右值引用(见 1.),它变成对Buffer
. 的右值引用
- 对
Buffer
的右值引用匹配Buffer
的移动构造函数,该构造函数用于构造make_zeroed_buffer()
return 的Buffer
对象价值。因此,return 值是通过从对象buffer
. 的
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
。