作为
@gudok 之前回答过的, 一切都在实现中... 然后一点是在用户代码中。
实现
假设我们正在谈论复制构造函数,将一个值分配给当前类。
您提供的实现将考虑以下两种情况:
- 参数是左值,因此根据定义,不能更改它
- 参数是右值,因此临时值在您使用它后不会再存在太长时间,因此您可以窃取其内容而不是复制其内容
这两种情况都使用重载来实现:
Box::Box(const Box & other)
{
}
Box::Box(Box && other)
{
}
轻量级类的实现
假设你的类包含两个整数: 你不能窃取它们,因为它们是原始的值。唯一看起来像窃取的事情就是复制这些值,然后将原始值设置为零,或者类似于这样的操作... 对于简单的整数来说这没有任何意义。为什么要做额外的工作呢?
因此,对于轻量级值类,实际上提供两个特定的实现,一个用于左值,一个用于右值,是没有意义的。
只提供l-value实现将足以满足需求。
重量级类的实现
但是在某些重量级类(即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;
Box box3 = std::move(box1);
对于box2来说,由于box1是一个左值,因此会调用第一个“慢速”复制构造函数。这是正常的C++98代码。
现在,对于box3,有趣的事情发生了:std::move确实返回相同的box1,但作为右值引用而不是左值。因此,该行代码为:
Box box3 = ...
...不会调用box1的复制构造函数。
它将在box1上调用窃取构造函数(正式称为移动构造函数)。
由于Box的移动构造函数“窃取”了box1的内容,因此在表达式结束时,box1处于有效但未指定的状态(通常为空),而box3包含box1的(先前的)内容。
移出类的有效但未指定状态怎么样?
当然,对l-value使用std::move意味着您承诺不再使用该l-value。或者您将非常、非常小心地使用它。
引用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-value,因此将调用移动构造函数。
对于代码作者(和代码审查者),代码实际上告诉它可以安全地窃取box1的内容,将其移动到box3中。代码作者随后必须确保不再使用box1(或非常小心地使用)。这是他们的责任。
但最终,移动构造函数的实现将产生巨大的性能差异:如果移动构造函数实际上窃取了r-value的内容,则会看到差异。如果它执行其他任何操作,则作者就在撒谎,但这是另一个问题...
std::move(box1);
并没有移动任何东西;然而Box b = std::move(box1);
确实 移动了一些东西。区别在于移动是由b
的初始化引起的,而不是由std::move
的调用引起的。 - M.M