移动物体比复制更快的原因是什么?

时间:2016-04-24 19:31:03

标签: c++ move rvalue

我听过斯科特迈耶斯说" 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()实施或任何语法内容。

3 个答案:

答案 0 :(得分:42)

作为@gudok answered before,一切都在实现中......然后在用户代码中有一点。

实施

我们假设我们正在讨论复制构造函数,以便为当前类赋值。

您提供的实施将考虑两种情况:

  1. 参数是l值,因此您无法按照定义
  2. 触摸它
  3. 参数是一个r值,所以,隐含地说,临时胜利的时间远远超过你使用它,因此,你可以窃取其内容而不是复制其内容
  4. 两者都是使用重载实现的:

    Box::Box(const Box & other)
    {
       // copy the contents of other
    }
    
    Box::Box(Box && other)
    {
       // steal the contents of other
    }
    

    灯光类的实现

    让我们说你的班级包含两个整数:你不能窃取那些因为它们是纯粹的原始价值。 似乎,如窃取的唯一方法是复制值,然后将原始设置为零,或类似的东西...这对简单整数没有意义。为什么这个额外工作?

    因此对于轻量级类,实际上提供两个特定的实现,一个用于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
    }
    

    它的普通构造函数(复制构造函数,需要分配)比另一个构造函数慢得多(move-constructor,只需要指定原始指针)。

    何时可以安全地窃取"?

    事情是:默认情况下,编译器将调用"快速代码"只有当参数是临时的时候(它有点微妙,但请耐心等待......)。

    为什么?

    因为编译器可以保证你可以从某个对象中偷窃而没有任何问题如果该对象是临时对象(或者无论如何都会很快被销毁)。对于其他对象,窃取意味着您突然有一个有效的对象,但处于未指定的状态,这仍然可以在代码中进一步使用。可能导致崩溃或错误:

    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是l值,第一个是&#34;慢&#34;复制构造函数被调用。这是正常的C ++ 98代码。

    现在,对于box3,会发生一些有趣的事情:std :: move确实返回相同的box1,但作为r值引用,而不是l值。这一行:

    Box box3 = ...
    

    ...不会在box1上调用copy-constructor。

    它会在box1上调用INSTEAD窃取构造函数(官方称为move-constructor)。

    因为你对Box的移动构造函数的实现确实&#34;偷了&#34; box1的内容,在表达式的末尾,box1处于有效但未指定的状态(通常,它将为空),box3包含box1的(previous)内容。

    移出的类的有效但未指定的状态怎么样?

    当然,在l值上编写std :: move意味着你做出了一个承诺,你再也不会使用那个l值。或者你会非常非常小心地去做。

    引用C ++ 17标准草案(C ++ 11:17.6.5.15):

      

    20.5.5.15从库类型的状态移出[lib.types.movedfrom]

         

    可以从(15.8)移动C ++标准库中定义的类型的对象。可以显式指定或隐式生成移动操作。除非另有规定,否则此类移动对象应置于有效但未指定的状态。

    这是关于标准库中的类型,但您应该遵循自己的代码。

    这意味着移出的值现在可以保持任何值,从空,零或某个随机值。例如。就你所知,你的字符串&#34;你好&#34;如果实施者认为这是正确的解决方案,那么它将成为一个空字符串&#34;&#34;或成为&#34;地狱&#34;甚至&#34;再见&#34;但它仍然必须是一个有效的字符串,并且所有的不变量都得到尊重。

    所以,最后,除非实施者(某种类型)在移动后明确地承诺了特定行为,否则你应该表现得好像你知道没有关于移出的值(那种类型)。

    结论

    如上所述,std :: move确实没有。它只告诉编译器:&#34;你看到l值?请将它视为一个r值,只需一秒钟即可。

    所以,在:

    Box box3 = std::move(box1); // ???
    

    ...用户代码(即std :: move)告诉编译器参数可以被视为该表达式的r值,因此,将调用移动构造函数。

    对于代码作者(和代码审阅者),代码实际上告诉他可以窃取box1的内容,将其移动到box3中。然后代码作者必须确保不再使用box1(或非常小心地使用)。这是他们的责任。

    但最后,移动构造函数的实现会产生影响,主要表现在性能上:如果移动构造函数实际上窃取了r值的内容,那么你会发现差异。如果它做了其他任何事情,那么作者撒了谎,但这是另一个问题......

答案 1 :(得分:15)

一切都与实施有关。考虑简单的字符串类:

class my_string {
  char* ptr;
  size_t capacity;
  size_t length;
};

copy 的语义要求我们制作完整的字符串副本,包括在动态内存中分配另一个数组并在那里复制*ptr内容,这是很昂贵的。

move 的语义要求我们只将指针本身的值传递给新对象而不复制字符串的内容。

当然,如果类不使用动态内存或系统资源,那么在性能方面移动和复制之间没有区别。

答案 2 :(得分:1)

std::move()函数应理解为对应rvalue类型的强制转换启用移动对象而不是复制。

根本没有任何区别:

std::cout << std::move(std::string("Hello, world!")) << std::endl;

这里,字符串已经是右值,因此std::move()没有改变任何内容。

它可能会启用移动,但仍可能会产生副本:

auto a = 42;
auto b = std::move(a);

没有更有效的方法来创建一个简单地复制它的整数。

导致移动发生的地方是参数

  1. 是左值或左值参考,
  2. 移动构造函数移动作业运算符,
  3. 是(隐式或明确地)构造或作业的来源。
  4. 即使在这种情况下,实际上移动数据也不是move()本身,而是构造或赋值。 std:move()只是允许发生这种情况的演员,即使你有一个左值。如果你从一个右值开始,那么移动可以在没有std::move的情况下发生。我认为这是迈耶斯声明背后的含义。