我听过斯科特迈耶斯说" std::move()什么都不动"......但我还没明白这意味着什么.
所以要指出我的问题,请考虑以下事项:
class Box { /* things... */ };
Box box1 = some_value;
Box box2 = box1; // value of box1 is copied to box2 ... ok
Run Code Online (Sandbox Code Playgroud)
关于什么:
Box box3 = std::move(box1);
Run Code Online (Sandbox Code Playgroud)
我确实理解左值和左值的规则,但我不明白的是记忆中实际发生了什么?它只是以某种不同的方式复制价值,共享地址或什么?更具体地说:什么使移动比复制更快?
我只是觉得理解这一点会让一切都清楚.提前致谢!
编辑:请注意,我不是在询问std::move()实现或任何语法的东西.
pae*_*bal 49
正如@gudok 之前回答的那样,一切都在实现中......然后有一点在用户代码中.
让我们假设我们正在讨论复制构造函数来为当前类赋值.
您将提供的实施将考虑两种情况:
两者都是使用重载实现的:
Box::Box(const Box & other)
{
// copy the contents of other
}
Box::Box(Box && other)
{
// steal the contents of other
}
Run Code Online (Sandbox Code Playgroud)
假设你的类包含两个整数:你不能窃取它们,因为它们是纯粹的原始值.唯一看起来像偷东西的是复制值,然后将原始设置为零,或类似的东西......这对于简单的整数是没有意义的.为什么要额外工作?
因此对于轻量级类,实际上提供两个特定的实现,一个用于l值,一个用于r值,没有任何意义.
仅提供l值实现将绰绰有余.
但是在一些重类(即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
}
Run Code Online (Sandbox Code Playgroud)
简单的一个构造函数(复制构造函数,需要分配)比另一个构造函数慢得多(移动构造函数,只需要指定原始指针).
问题是:默认情况下,只有当参数是临时参数时,编译器才会调用"快速代码"(它有点微妙,但请耐心等待......).
为什么?
因为编译器可以保证只有当该对象是临时对象时才可以从某个对象中偷取而没有任何问题(或者无论如何都会很快被销毁).对于其他对象,窃取意味着您突然有一个有效的对象,但处于未指定的状态,这仍然可以在代码中进一步使用.可能导致崩溃或错误:
Box box3 = static_cast<Box &&>(box1); // calls the "stealing" constructor
box1.doSomething(); // Oops! You are using an "empty" object!
Run Code Online (Sandbox Code Playgroud)
但有时候,你想要表现.你是怎么做到的?
正如你写的:
Box box1 = some_value;
Box box2 = box1; // value of box1 is copied to box2 ... ok
Box box3 = std::move(box1); // ???
Run Code Online (Sandbox Code Playgroud)
box2会发生的情况是,由于box1是l值,因此调用第一个"慢"复制构造函数.这是正常的C++ 98代码.
现在,对于box3,会发生一些有趣的事情:std :: move确实返回相同的box1,但是作为r值引用,而不是l值.所以行:
Box box3 = ...
Run Code Online (Sandbox Code Playgroud)
...不会在box1上调用copy-constructor.
它将在box1上调用INSTEAD窃取构造函数(官方称为move-constructor).
并且,当你对Box的移动构造函数的实现"窃取"box1的内容时,在表达式的末尾,box1处于有效但未指定的状态(通常,它将为空),而box3包含(之前的) box1的内容.
当然,在l值上编写std :: move意味着你做出一个承诺,你不会再使用那个l值.或者你会非常,非常小心地去做.
引用C++ 17标准草案(C++ 11:17.6.5.15):
20.5.5.15库类型的移动状态[lib.types.movedfrom]
可以从(15.8)移动C++标准库中定义的类型的对象.可以显式指定或隐式生成移动操作.除非另有规定,否则此类移动物体应置于有效但未指定的状态.
这是关于标准库中的类型,但您应该遵循自己的代码.
这意味着移出的值现在可以保持任何值,从空,零或一些随机值.例如,如果实施者认为这是正确的解决方案,那么您的字符串"Hello"将变为空字符串"",或变为"地狱",甚至"再见".但它仍然必须是一个有效的字符串,并且它的所有不变量都受到尊重.
因此,最后,除非实现者(一种类型)在移动后明确地提交给特定行为,否则你应该表现得好像你对移出的值(该类型)一无所知.
如上所述,std :: move 什么都不做.它只告诉编译器:"你看到那个l值?请把它视为一个r值,只是一秒钟".
所以,在:
Box box3 = std::move(box1); // ???
Run Code Online (Sandbox Code Playgroud)
...用户代码(即std :: move)告诉编译器参数可以被视为该表达式的r值,因此,将调用移动构造函数.
对于代码作者(和代码审阅者),代码实际上告诉可以窃取box1的内容,将其移动到box3中.然后代码作者必须确保不再使用box1(或非常小心地使用).这是他们的责任.
但最终,移动构造函数的实现会产生影响,主要表现在性能上:如果移动构造函数实际上窃取了r值的内容,那么你会发现差异.如果它做了什么,那么作者撒了谎,但这是另一个问题......
gud*_*dok 15
这都是关于实施的.考虑简单的字符串类:
class my_string {
char* ptr;
size_t capacity;
size_t length;
};
Run Code Online (Sandbox Code Playgroud)
复制的语义要求我们制作字符串的完整副本,包括在动态存储器中分配另一个数组并在*ptr那里复制内容,这是很昂贵的.
移动的语义要求我们只将指针本身的值传递给新对象而不复制字符串的内容.
当然,如果类不使用动态内存或系统资源,那么在性能方面移动和复制之间没有区别.