是什么让移动对象比复制更快?
What makes moving objects faster than copying?
我听 Scott Meyers 说“std::move()
什么都没动”......但我不明白这是什么意思。
所以要具体说明我的问题,请考虑以下几点:
class Box { /* things... */ };
Box box1 = some_value;
Box box2 = box1; // value of box1 is copied to box2 ... ok
怎么样:
Box box3 = std::move(box1);
我明白左值和右值的规则,但我不明白的是内存中到底发生了什么?它只是以某种不同的方式复制价值,共享地址还是什么?更具体地说:是什么让移动比复制更快?
我只是觉得理解这一点会让我明白一切。提前致谢!
编辑: 请注意,我不是在询问 std::move()
实现或任何语法内容。
关键在于执行。考虑简单的字符串 class:
class my_string {
char* ptr;
size_t capacity;
size_t length;
};
copy 的语义要求我们制作字符串的完整副本,包括在动态内存中分配另一个数组并在那里复制 *ptr
内容,这是昂贵的。
move的语义要求我们只将指针本身的值传递给新对象,而不复制字符串的内容。
当然,如果class不使用动态内存或系统资源,那么移动和复制在性能方面没有区别。
作为@gudok ,一切都在实现中...然后一点在用户代码中。
实施
假设我们正在谈论复制构造函数为当前class赋值。
您提供的实施将考虑两种情况:
- 该参数是一个左值,因此根据定义,您不能触摸它
- 该参数是一个 r 值,因此,隐含地,临时文件在您使用它之后不会存在太久,因此,您可以窃取其内容,而不是复制其内容
两者都是使用重载实现的:
Box::Box(const Box & other)
{
// copy the contents of other
}
Box::Box(Box && other)
{
// steal the contents of other
}
灯光的实现 classes
假设您的 class 包含两个整数:您不能 窃取 它们,因为它们是原始值。唯一 看起来 像 窃取 的方法是复制值,然后将原始值设置为零,或类似的东西......哪个对于简单的整数没有意义。为什么要做这些额外的工作?
所以对于轻值 classes,实际上提供两种具体实现,一种用于左值,一种用于右值,是没有意义的。
仅提供左值实现就足够了。
较重 classes
的实施
但在某些重 classes 的情况下(即 std::string、std::map 等),复制意味着潜在的成本,通常在分配中。所以,理想情况下,你要尽可能避免它。这就是窃取临时数据变得有趣的地方。
假设您的 Box 包含指向 HeavyResource
的原始指针,复制成本很高。代码变为:
Box::Box(const Box & other)
{
this->p = new HeavyResource(*(other.p)) ; // costly copying
}
Box::Box(Box && other)
{
this->p = other.p ; // trivial stealing, part 1
other.p = nullptr ; // trivial stealing, part 2
}
很明显,一个构造函数(复制构造函数,需要分配)比另一个构造函数(移动构造函数,只需要分配原始指针)慢得多。
什么时候去 "steal" 安全?
问题是:默认情况下,编译器只会在参数是临时参数时调用 "fast code"(这有点微妙,但请耐心等待...)。
为什么?
因为编译器可以保证你可以毫无问题地从某个对象中窃取只有如果那个对象是临时的(或者很快就会被销毁)。对于其他对象,窃取意味着您突然拥有一个有效但处于未指定状态的对象,该对象仍可在代码中进一步使用。可能导致崩溃或错误:
Box box3 = static_cast<Box &&>(box1); // calls the "stealing" constructor
box1.doSomething(); // Oops! You are using an "empty" object!
但有时,您想要性能。那么,你是怎么做到的?
用户密码
如您所写:
Box box1 = some_value;
Box box2 = box1; // value of box1 is copied to box2 ... ok
Box box3 = std::move(box1); // ???
box2 发生的情况是,由于 box1 是左值,因此调用第一个 "slow" 复制构造函数。这是正常的 C++98 代码。
现在,对于 box3,有趣的事情发生了:std::move 做 return 相同的 box1,但作为右值参考,而不是左值。所以行:
Box box3 = ...
...不会在 box1 上调用复制构造函数。
它将在 box1 上调用 INSTEAD 窃取构造函数(正式称为移动构造函数)。
当您对 Box 的移动构造函数的实现执行 "steal" box1 的内容时,在表达式的末尾,box1 处于有效但未指定的状态(通常为空), box3 包含 box1 的(先前)内容。
搬出的有效但未指定的状态如何 class?
当然,在左值上写 std::move 意味着您承诺不再使用该左值。或者你会做的,非常非常小心。
引用 C++17 标准草案(C++11 为:17.6.5.15):
20.5.5.15 Moved-from state of library types [lib.types.movedfrom]
Objects of types defined in the C++ standard library may be moved from (15.8). Move operations may be explicitly specified or implicitly generated. Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state.
这是关于标准库中的类型的,但这是您在自己的代码中应该遵循的内容。
这意味着移出的值现在可以包含任何值,可以是空值、零值或某个随机值。例如。如您所知,如果实施者认为这是正确的解决方案,您的字符串 "Hello" 将变为空字符串 "",或者变为 "Hell",甚至 "Goodbye"。不过,它仍然必须是一个有效的字符串,并遵守其所有不变量。
所以,最后,除非(某种类型的)实施者在移动后明确承诺特定行为,否则您应该表现得就好像您对移动一无所知一无所知 -out 值(该类型)。
结论
如上所述,std::move 什么都不做。它只告诉编译器:"You see that l-value? please consider it a r-value, just for a second".
所以,在:
Box box3 = std::move(box1); // ???
... 用户代码(即 std::move)告诉编译器可以将参数视为此表达式的右值,因此将调用移动构造函数。
对于代码作者(和代码审查者)来说,代码实际上告诉我们可以窃取 box1 的内容,将其移动到 box3 中。然后代码作者必须确保不再使用 box1(或非常非常小心地使用)。这是他们的责任。
但最终,移动构造函数的实现会有所不同,主要是在性能方面:如果移动构造函数实际上窃取了 r 值的内容,那么您会看到不同之处。如果它做了其他任何事情,那么作者就此撒了谎,但这是另一个问题...
std::move()
函数应该理解为 cast 到相应的右值类型,即 启用 移动对象复制。
可能完全没有区别:
std::cout << std::move(std::string("Hello, world!")) << std::endl;
这里,字符串已经是一个右值,所以std::move()
没有改变任何东西。
它可能会启用移动,但仍可能导致复制:
auto a = 42;
auto b = std::move(a);
没有比简单复制更有效的创建整数的方法了。
将导致移动发生的地方是参数
- 是一个左值,或左值引用,
- 有一个移动构造函数或移动赋值运算符,并且
- 是(隐式或显式)构造或赋值的来源。
即使在这种情况下,实际上 移动 数据的也不是 move()
本身,而是构造或赋值。 std:move()
只是允许这种情况发生的强制转换,即使您有一个左值开始。如果您从右值开始,则移动可以在没有 std::move
的情况下发生。我认为这就是迈耶斯声明背后的含义。
我听 Scott Meyers 说“std::move()
什么都没动”......但我不明白这是什么意思。
所以要具体说明我的问题,请考虑以下几点:
class Box { /* things... */ };
Box box1 = some_value;
Box box2 = box1; // value of box1 is copied to box2 ... ok
怎么样:
Box box3 = std::move(box1);
我明白左值和右值的规则,但我不明白的是内存中到底发生了什么?它只是以某种不同的方式复制价值,共享地址还是什么?更具体地说:是什么让移动比复制更快?
我只是觉得理解这一点会让我明白一切。提前致谢!
编辑: 请注意,我不是在询问 std::move()
实现或任何语法内容。
关键在于执行。考虑简单的字符串 class:
class my_string {
char* ptr;
size_t capacity;
size_t length;
};
copy 的语义要求我们制作字符串的完整副本,包括在动态内存中分配另一个数组并在那里复制 *ptr
内容,这是昂贵的。
move的语义要求我们只将指针本身的值传递给新对象,而不复制字符串的内容。
当然,如果class不使用动态内存或系统资源,那么移动和复制在性能方面没有区别。
作为@gudok
实施
假设我们正在谈论复制构造函数为当前class赋值。
您提供的实施将考虑两种情况:
- 该参数是一个左值,因此根据定义,您不能触摸它
- 该参数是一个 r 值,因此,隐含地,临时文件在您使用它之后不会存在太久,因此,您可以窃取其内容,而不是复制其内容
两者都是使用重载实现的:
Box::Box(const Box & other)
{
// copy the contents of other
}
Box::Box(Box && other)
{
// steal the contents of other
}
灯光的实现 classes
假设您的 class 包含两个整数:您不能 窃取 它们,因为它们是原始值。唯一 看起来 像 窃取 的方法是复制值,然后将原始值设置为零,或类似的东西......哪个对于简单的整数没有意义。为什么要做这些额外的工作?
所以对于轻值 classes,实际上提供两种具体实现,一种用于左值,一种用于右值,是没有意义的。
仅提供左值实现就足够了。
较重 classes
的实施但在某些重 classes 的情况下(即 std::string、std::map 等),复制意味着潜在的成本,通常在分配中。所以,理想情况下,你要尽可能避免它。这就是窃取临时数据变得有趣的地方。
假设您的 Box 包含指向 HeavyResource
的原始指针,复制成本很高。代码变为:
Box::Box(const Box & other)
{
this->p = new HeavyResource(*(other.p)) ; // costly copying
}
Box::Box(Box && other)
{
this->p = other.p ; // trivial stealing, part 1
other.p = nullptr ; // trivial stealing, part 2
}
很明显,一个构造函数(复制构造函数,需要分配)比另一个构造函数(移动构造函数,只需要分配原始指针)慢得多。
什么时候去 "steal" 安全?
问题是:默认情况下,编译器只会在参数是临时参数时调用 "fast code"(这有点微妙,但请耐心等待...)。
为什么?
因为编译器可以保证你可以毫无问题地从某个对象中窃取只有如果那个对象是临时的(或者很快就会被销毁)。对于其他对象,窃取意味着您突然拥有一个有效但处于未指定状态的对象,该对象仍可在代码中进一步使用。可能导致崩溃或错误:
Box box3 = static_cast<Box &&>(box1); // calls the "stealing" constructor
box1.doSomething(); // Oops! You are using an "empty" object!
但有时,您想要性能。那么,你是怎么做到的?
用户密码
如您所写:
Box box1 = some_value;
Box box2 = box1; // value of box1 is copied to box2 ... ok
Box box3 = std::move(box1); // ???
box2 发生的情况是,由于 box1 是左值,因此调用第一个 "slow" 复制构造函数。这是正常的 C++98 代码。
现在,对于 box3,有趣的事情发生了:std::move 做 return 相同的 box1,但作为右值参考,而不是左值。所以行:
Box box3 = ...
...不会在 box1 上调用复制构造函数。
它将在 box1 上调用 INSTEAD 窃取构造函数(正式称为移动构造函数)。
当您对 Box 的移动构造函数的实现执行 "steal" box1 的内容时,在表达式的末尾,box1 处于有效但未指定的状态(通常为空), box3 包含 box1 的(先前)内容。
搬出的有效但未指定的状态如何 class?
当然,在左值上写 std::move 意味着您承诺不再使用该左值。或者你会做的,非常非常小心。
引用 C++17 标准草案(C++11 为:17.6.5.15):
20.5.5.15 Moved-from state of library types [lib.types.movedfrom]
Objects of types defined in the C++ standard library may be moved from (15.8). Move operations may be explicitly specified or implicitly generated. Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state.
这是关于标准库中的类型的,但这是您在自己的代码中应该遵循的内容。
这意味着移出的值现在可以包含任何值,可以是空值、零值或某个随机值。例如。如您所知,如果实施者认为这是正确的解决方案,您的字符串 "Hello" 将变为空字符串 "",或者变为 "Hell",甚至 "Goodbye"。不过,它仍然必须是一个有效的字符串,并遵守其所有不变量。
所以,最后,除非(某种类型的)实施者在移动后明确承诺特定行为,否则您应该表现得就好像您对移动一无所知一无所知 -out 值(该类型)。
结论
如上所述,std::move 什么都不做。它只告诉编译器:"You see that l-value? please consider it a r-value, just for a second".
所以,在:
Box box3 = std::move(box1); // ???
... 用户代码(即 std::move)告诉编译器可以将参数视为此表达式的右值,因此将调用移动构造函数。
对于代码作者(和代码审查者)来说,代码实际上告诉我们可以窃取 box1 的内容,将其移动到 box3 中。然后代码作者必须确保不再使用 box1(或非常非常小心地使用)。这是他们的责任。
但最终,移动构造函数的实现会有所不同,主要是在性能方面:如果移动构造函数实际上窃取了 r 值的内容,那么您会看到不同之处。如果它做了其他任何事情,那么作者就此撒了谎,但这是另一个问题...
std::move()
函数应该理解为 cast 到相应的右值类型,即 启用 移动对象复制。
可能完全没有区别:
std::cout << std::move(std::string("Hello, world!")) << std::endl;
这里,字符串已经是一个右值,所以std::move()
没有改变任何东西。
它可能会启用移动,但仍可能导致复制:
auto a = 42;
auto b = std::move(a);
没有比简单复制更有效的创建整数的方法了。
将导致移动发生的地方是参数
- 是一个左值,或左值引用,
- 有一个移动构造函数或移动赋值运算符,并且
- 是(隐式或显式)构造或赋值的来源。
即使在这种情况下,实际上 移动 数据的也不是 move()
本身,而是构造或赋值。 std:move()
只是允许这种情况发生的强制转换,即使您有一个左值开始。如果您从右值开始,则移动可以在没有 std::move
的情况下发生。我认为这就是迈耶斯声明背后的含义。