编译器何时在C ++中移动/复制?

时间:2016-08-04 15:24:45

标签: c++ c++11

我试图更好地理解移动在C ++中是如何工作的。编译器如何知道何时移动以及何时不移动?

#include <iostream>

template <typename T>
struct Container {
    T value;
    Container (T value): value(value) {}
    Container (const Container & i): value(i.value) {
        std::cout << "copying" << std::endl;
    }
    Container (Container && i): value(std::move(i.value)) {
        std::cout << "moving" << std::endl;
    }
};

void increment (Container<int> n) {
    std::cout << "incremented to " << ++n.value << std::endl;
}

int main () {
    Container<int> n (5);
    increment(std::move(n));
    std::cout << n.value << std::endl;
}

此示例打印

moving
incremented to 6
5

所以我希望int被移动,但之后我不能再使用它(并获得原始值)。

好的,也许int被复制了,因为value(std::move(i.value))将其复制到移动构造函数中。但是我仍然不明白为什么Container<int> n在被它移动之后仍然存在。

5 个答案:

答案 0 :(得分:5)

std::move实际上只是将值更改为可以移动的右值。如果你将它应用于int它没有任何实际效果,因为它对#34;移动&#34;没有任何意义。 intint。 (确实,该值仍然是一个右值;它只是具有对int的右值引用通常不比对{{1}的任何其他类型的引用更有用。 }})。

这是因为移动意味着将资源从一个对象传输到另一个对象,从而避免了复制这些资源(通过复制它们)的需要 - 因为这样的复制可以是不平凡的;一方面,它可能需要动态内存分配。复制int值非常简单,因此不需要特殊的移动语义。

因此,应用于您的示例,移动Container<int>与复制它完全相同,当然除了输出(&#34;移动&#34; vs&#34;复制&#34;)

(请注意,即使 move 也要求源对象在操作完成后保持有效状态 - 它不会破坏源对象,因为您似乎认为它可能是应)。

关于编译器如何知道何时可以移动vs复制,这是类型类别的问题。您使用std::move专门将值类型类别更改为 rvalue (或更具体地说,更改为 xvalue ),此类型的值可以与rvalue匹配移动构造函数中的引用参数。通常,具有右值参考参数的重载优先于具有非右值参考参数的重载(精确的规则很复杂)。

产生右值的另一种常见方法是作为一个未命名的临时值 - 通过对对象或值执行某些操作,其中结果不会绑定到变量(a + b,其中{{1} }和a都是b类型,是一个简单的例子 - 结果是一个临时对象;它不存在于自己的变量中)。当一个更复杂的对象是临时对象时,将其移动到其最终目标可能比复制它更有效,并且是安全的,因为之后不能使用移动对象的不确定状态。因此,这些值也是rvalues并且将绑定到右值引用(并且可以被移动)。

答案 1 :(得分:2)

  

所以我希望int被移动

int这样的原始类型没有移动构造函数,只需复制它们。

  

但之后我不能再使用它

这不是移动的方式。您仍然可以使用移动的对象 - 它将处于有效状态。但是,该状态是不确定的,因此您不能指望该对象具有任何特定值,也不能使用具有前置条件的操作。据我所知,int个对象上没有对前提条件的操作。

  

好的,也许int被复制了,因为value(std :: move(i.value))将它复制到移动构造函数中。

是的,它被复制了。

  

但是我仍然不明白为什么容器n在被它移动之后仍然存在。

移动后物体不会消失。一般来说,它们处于有效但不确定的状态。您编写的这个特定的移动构造函数恰好使对象处于与移动之前完全相同的状态。

  

编译器何时在C ++中移动

使用非const rvalue参数调用复制初始化时,对象的类型具有移动构造函数。或者,为对象分配非const r值操作数,并且对象的类型具有移动赋值运算符。

  

编译器如何知道何时移动以及何时不移动?

编译器知道所有表达式的类型。特别是,它知道表达式的类型是否是非const rvalue。编译器还知道您可以移动的所有类型的定义。根据定义,编译器知道类型是否具有移动构造函数。

答案 2 :(得分:1)

移动只对具有非平凡状态的对象有意义,复制起来会很昂贵。在移动结束时,原始值和新值都应处于有效状态。新值应该等于旧值的值,并且您通常不知道旧值的状态是什么,而不是有效。

例如,使用向量,将内容从一个复制到另一个是昂贵的,因此移动只会交换内容。这样效率更高,并且具有不会失败的奖励。移动后,您仍然可以使用旧向量,但它不会处于移动前的状态。

答案 3 :(得分:1)

  

所以我希望int被移动,但之后我不能再使用它(并获得原始值)。

A&#34;移动&#34;在C ++中实际上并没有移动内存。对于像字符串和向量这样的复杂类型,&#34;移动&#34;表示内部指针被交换而不是复制所有指向的数据。但是对于int,没有可以做到的捷径,因此只需复制数据。这就是为什么你的原始值仍然存在,你仍然可以阅读它。

  

C ++编译器如何知道何时移动以及何时不移动?

它没有!

可移动课程的作者。通常,你把这个&#34;指针交换&#34;将逻辑转换为&#34;移动构造函数&#34;,这是一个采用rvalue-reference的构造函数的奇特名称。自C ++ 11以来的重载决策规则是这样设计的,这样只要你传入一个临时的(或者通过写std::move来传递你通过作弊得到的rvalue就会调用这个构造函数(它的名字非常简洁)不会移动任何东西!)),因为这些是你通常想要的物品#34;从&#34;移动。

但如果你不写任何&#34;移动&#34;移动构造函数中的代码然后编译器不会为你创造一些。

  

但是我仍然不明白为什么Container<int> n在被它移动之后仍然存在。

您没有编写任何代码,在将其传递给移动构造函数后,会将原始Container<int>保留在任何不同的状态。

同样,移动构造函数不会自动执行此类操作。它只是放置你的移动逻辑的地方!

这是Container的替代版本,可以观察移动

template <typename T>
struct Container
{
    T* ptr;
    Container(T value) : ptr(new T(value)) {}
    Container(const Container& i): ptr(new T(*i.ptr)) {
        std::cout << "copying" << std::endl;
    }
    Container(Container&& i) {
        ptr = i.ptr;
        i.ptr = nullptr;
        // no data copied - we just "steal" the data by swapping pointers!
        std::cout << "moving" << std::endl;
    }
    ~Container() { delete ptr; }
};

(我已经使用原始指针尽可能清楚地了解正在发生的事情;实际上你会使用std::unique_ptr实际上为你做同样的事情在其自己的移动构造函数中!)

答案 4 :(得分:0)

移动对象后,标准中没有任何内容表明您之后无法使用它。顶级的C ++开发人员,比如Sean Parent,已经抱怨过这个并且想要更好的东西&#34;更好的&#34;由标准强制执行。移动对象旨在作为优化,并且可以将移动的对象保留在任何状态。因此,移动基元可以由编译器实现为直接复制,只留下原始值。

为了更好地了解如何在不编写复杂对象的情况下移动对象,只需使用填充了少量值的std :: vector。在第一次电话会议中,我们正在建立一个基本案例。向量通过引用传递给const,因此在函数的参数上调用复制构造函数。在第二次调用中,向量作为右值引用传递,因为当我们调用std :: move时,我们就这样传递了它。在这种情况下,将调用向量的移动构造函数。在内部,新值(函数的参数)从传入的右值引用中获取内部指针。然后它将传入的向量内部指针设置为类似于空向量的内容。我们在第三次调用中看到了这一点,我们制作另一个副本。但是,向量在第二个调用中被移动,因此第三个调用的副本中没有元素。

#include <iostream>
#include <vector>

void print_size(std::vector<int> value) {
    std::cout << value.size() << std::endl;
}

int main() {
    std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    std::cout << "Pass by reference to const: ";
    print_size(v);
    std::cout << "Pass by rvalue reference: ";
    print_size(std::move(v));
    std::cout << "Pass by reference to const: ";
    print_size(v);
    return 0;
}