作为@gudokanswered before,一切都在实现中......然后在用户代码中。
实现
假设我们正在讨论为当前类分配值的复制构造函数。
您将提供的实现将考虑两种情况:
- 参数是一个左值,所以你不能触摸它,根据定义
- 该参数是一个 r 值,因此,隐含地,在您使用它之后,临时变量的寿命不会更长,因此,您可以窃取其内容,而不是复制其内容
两者都是使用重载实现的:
Box::Box(const Box & other)
{
// copy the contents of other
}
Box::Box(Box && other)
{
// steal the contents of other
}
轻类的实现
假设您的类包含两个整数:您不能窃取它们,因为它们是普通的原始值。唯一看起来像窃取的是复制值,然后将原始值设置为零,或类似的东西......这对于简单的整数没有意义.为什么要做这些额外的工作?
所以对于轻值类,实际上提供两种具体的实现,一种用于左值,一种用于右值,是没有意义的。
只提供左值实现就绰绰有余了。
较重类的实现
但在某些重类(即 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
}
很明显,一个构造函数(复制构造函数,需要分配)比另一个(移动构造函数,只需要分配原始指针)慢得多。
什么时候“偷”是安全的?
事情是:默认情况下,编译器只会在参数是临时参数时调用“快速代码”(它有点微妙,但请耐心等待......)。
为什么?
因为编译器可以保证你可以毫无问题地从某个对象中窃取只有如果该对象是临时的(或者无论如何都会很快被销毁)。对于其他对象,窃取意味着您突然拥有一个有效但处于未指定状态的对象,该对象仍可在代码中进一步使用。可能导致崩溃或错误:
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 是左值,因此调用了第一个“慢”复制构造函数。这是正常的 C++98 代码。
现在,对于 box3,发生了一些有趣的事情:std::move 确实返回了相同的 box1,但作为 r 值引用,而不是 l 值。所以这行:
Box box3 = ...
... 不会在 box1 上调用复制构造函数。
它将在box1上调用INSTEAD窃取构造函数(官方称为move-constructor)。
并且由于您对 Box 的移动构造函数的实现确实“窃取”了 box1 的内容,在表达式的末尾,box1 处于有效但未指定的状态(通常为空),并且 box3 包含(previous) box1 的内容。
移出班级的有效但未指定的状态如何?
当然,在左值上编写 std::move 意味着您承诺不再使用该左值。或者你会非常非常小心地这样做。
引用 C++17 标准草案(C++11 为:17.6.5.15):
20.5.5.15 库类型的移动状态 [lib.types.movedfrom]
在 C++ 标准库中定义的类型的对象可以从 (15.8) 中移出。移动操作可以显式指定或隐式生成。除非另有说明,否则此类移出的对象应处于有效但未指定的状态。
这是关于标准库中的类型的,但这是您自己的代码应该遵循的。
这意味着移出的值现在可以保存任何值,可以是空值、零值或某个随机值。例如。据你所知,如果实施者认为这是正确的解决方案,你的字符串“Hello”将变成一个空字符串“”,或者变成“Hell”,甚至是“Goodbye”。不过,它仍然必须是一个有效的字符串,并尊重其所有不变量。
因此,最后,除非(某个类型的)实现者在移动后明确承诺特定行为,否则您应该表现得好像您对移出的值(的那种)。
结论
如上所述,std::move 什么都不做。它只告诉编译器:“你看到那个左值了吗?请考虑一下它是右值”。
所以,在:
Box box3 = std::move(box1); // ???
...用户代码(即std::move)告诉编译器参数可以被认为是这个表达式的r值,因此将调用move构造函数。
对于代码作者(和代码审阅者)来说,代码实际上告诉可以窃取 box1 的内容,将其移动到 box3 中。然后,代码作者必须确保不再使用 box1(或非常小心地使用)。这是他们的责任。
但最终,移动构造函数的实现会有所不同,主要是在性能方面:如果移动构造函数实际上窃取了 r 值的内容,那么你会看到不同之处。如果它做了别的什么,那么作者就撒谎了,但这是另一个问题......