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

When does the compiler move/copy in C++?

本文关键字:移动 复制 C++ 何时 编译器      更新时间:2023-10-16

我正在努力更好地理解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))在move构造函数中复制了它。但我仍然不明白为什么Container<int> n在它肯定被移动后仍然存在。

std::move实际上只是将一个值更改为一个可以移动的右值。如果将此应用于int,则没有实际效果,因为"移动"int值没有任何意义。(确实,该值仍然是一个右值;只是对int进行右值引用通常不会比对int进行任何其他类型的引用更有用)。

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

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

(请注意,即使是移动,也需要源对象在操作完成后保持有效状态——它不会破坏源对象,就像你认为的那样)。

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

生成右值的另一种常见方式是作为未命名的临时对象,即对结果未绑定到变量的对象或值执行一些操作(a + b是一个简单的例子,其中ab都属于int类型,结果是一个临时对象;它不存在于自己的变量中)。当一个更复杂的对象是临时的时,将其移动到其最终目的地可能比复制它更有效,而且是安全的,因为从中移动的对象的不确定状态以后无法使用。因此,这些值也是右值,并且将绑定到右值引用(并且可能被移动)。

所以我希望int已经被移动了

int这样的基元类型没有move构造函数,它们只是被复制的。

但是之后我就不能再使用了

搬家不是这样的。你仍然可以很好地使用一个移动的对象——它将处于有效状态。但是,该状态是不确定的,因此不能期望对象具有任何特定值,也不能使用具有先决条件的操作。据我所知,int对象上没有任何具有先决条件的操作。

好吧,也许int被复制是因为value(std::move(i.value))在move构造函数中复制了它。

是的,它是复制的。

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

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


编译器什么时候移动。。。在C++中

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

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

编译器知道所有表达式的类型。特别是,它知道表达式的类型是否为非常量右值。编译器还知道可以移动的所有类型的定义。根据定义,编译器知道一个类型是否有移动构造函数。

因此,我希望int已经被移动,但之后我应该无法继续使用它(并获得原始值)。

C++中的"移动"实际上并不能移动内存。对于字符串和向量等复杂类型,"移动"意味着交换内部指针,而不是复制所有指向的数据。但对于int来说,没有捷径可走,因此数据只需复制即可。这就是为什么你的原始价值仍然存在,你仍然可以阅读它

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

事实并非如此!

动产阶级的作者是这样做的。通常,您将这个"指针交换"逻辑放入一个"移动构造函数"中,这是一个接受右值引用的构造函数的奇特名称。C++11以来的重载解析规则是这样设计的,即每当您传入临时(或通过编写std::move(它的名称很糟糕,因为它不会移动任何东西!)传入通过欺骗获得的右值)时,都会调用此构造函数,因为这些是您通常想要"从中移动"的对象。

但是,如果你不在move构造函数中编写任何"move"代码,那么编译器就不会为你发明一些。

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

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

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

以下是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,它在自己的移动构造函数中为你做同样的事情!)

移动只对具有非琐碎状态的对象有意义,因为复制这些状态很昂贵。在移动结束时,原始值和新值都应处于有效状态。新值应该与旧值等效,并且您通常不知道旧值的状态除了有效之外是什么。

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

一旦移动了对象,标准中就没有任何内容表明以后不能使用它。顶级C++开发人员,如SeanPParent,对此表示不满,并希望通过该标准来执行"更好"的东西。移动对象是一种优化,可以使移动的对象处于任何状态。因此,移动基元可以由编译器作为直接副本来实现,而不保留原始值。

为了更好地了解在不编写复杂对象的情况下移动对象的工作原理,只需使用填充了几个值的std::向量即可。在第一次通话中,我们正在建立一个基本案例。向量是通过引用常量传递的,因此复制构造函数是在函数的参数上调用的。在第二个调用中,向量作为右值引用传递,因为我们在调用std::move时将其强制转换为右值引用。在这种情况下,将调用向量的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;
}