移动赋值运算符,移动构造函数

Move assignment operator, move constructor

我一直在努力确定 5 的规则,但网上的大部分信息都过于复杂,示例代码也不尽相同。

即使我的教科书也没有很好地涵盖这个主题。

关于移动语义:

撇开模板、右值和左值不谈,据我了解,移动语义很简单:

int other     = 0;           //Initial value
int number    = 3;           //Some data

int *pointer1 = &number;     //Source pointer
int *pointer2 = &other;      //Destination pointer

*pointer2     = *pointer1;   //Both pointers now point to same data 
 pointer1     =  nullptr;    //Pointer2 now points to nothing

//The reference to 'data' has been 'moved' from pointer1 to pointer2

相对于复制,这相当于这样的事情:

pointer1      = &number;     //Reset pointer1

int newnumber = 0;           //New address for the data

newnumber     = *pointer1;   //Address is assigned value
pointer2      =  &newnumber; //Assign pointer to new address

//The data from pointer1 has been 'copied' to pointer2, at the address 'newnumber'

不需要解释右值、左值或模板,我什至可以说这些主题是不相关的。

第一个示例比第二个示例更快这一事实应该是给定的。我还要指出,C++ 11 之前的任何高效代码都可以做到这一点。

根据我的理解,这个想法是将所有这些行为捆绑在标准库中一个简洁的小运算符 move() 中。

在编写拷贝构造函数和拷贝赋值运算符时,我简单地这样做:

Text::Text(const Text& copyfrom) {
    data  = nullptr;  //The object is empty
    *this = copyfrom;

}


const Text& Text::operator=(const Text& copyfrom) {
    if (this != &copyfrom) {
        filename = copyfrom.filename;
        entries  = copyfrom.entries;

        if (copyfrom.data != nullptr) {  //If the object is not empty
            delete[] data;
        }

        data = new std::string[entries];

        for (int i = 0; i < entries; i++) {
            data[i] = copyfrom.data[i];
            //std::cout << data[i];
        }
        std::cout << "Data is assigned" << std::endl;

    }

    return *this;
}

人们会认为,等效的是:

Text::Text(Text&& movefrom){
    *this = movefrom;
}

Text&& Text::operator=(Text&& movefrom) {
    if (&movefrom != this) {
        filename = movefrom.filename;
        entries  = movefrom.entries;
        data     = movefrom.data;

        if (data != nullptr) {
            delete[] data;
        }

        movefrom.data    = nullptr;
        movefrom.entries = 0;
    }
    return std::move(*this);
}

我很确定这行不通,所以我的问题是:如何使用移动语义实现这种类型的构造函数功能?

我不太清楚你的代码示例应该证明什么——或者这个问题的重点是什么。

概念上短语'move semantics'在C++中是什么意思?

是"how do I write move ctors and move assignment operators?"吗?

这是我尝试介绍的概念。如果您想查看代码示例,请查看评论中链接的任何其他 SO 问题。


直觉上,在 C 和 C++ 中,一个对象应该表示驻留在内存中的一段数据。出于多种原因,通常您想将该数据发送到其他地方。

通常可以采取一种直接的方法,将对象的指针/引用简单地传递到需要数据的地方。然后,可以使用指针读取它。获取指针并四处移动指针非常便宜,因此这通常非常有效。主要缺点是您必须确保该对象会在需要时一直存在,否则您会得到悬空指针/引用和崩溃。有时这很容易确保,有时则不然。

如果不是,一个明显的替代方法是复制并传递它(按值传递)而不是按引用传递。当需要数据的地方有自己的数据副本时,它可以确保副本在需要时一直存在。这里的主要缺点是你必须制作一个副本,如果对象很大,这可能会很昂贵。

第三种选择是移动对象而不是复制它。当一个对象被移动时,它不会被复制,而是在新站点中独占可用,而不再在旧站点中可用。显然,您只能在旧站点不再需要它时执行此操作,但在那种情况下,这会为您节省一份副本,这可以节省一大笔钱。

当对象很简单时,所有这些概念在实际实施和正确实施时都相当微不足道。例如,当你有一个 trivial 对象时,也就是说,一个具有简单构造/破坏的对象,可以安全地像在 C 编程语言中那样使用 memcpy 完全复制它。 memcpy 生成字节块的逐字节副本。如果一个普通对象被正确初始化,因为它的创建没有可能的副作用,而且它后来的销毁也没有,那么 memcpy 副本也被正确初始化并产生一个有效的对象。

然而,在现代 C++ 中,您的许多对象都不是微不足道的——它们可能 "own" 引用堆内存,并使用 RAII 管理此内存,RAII 将对象的生命周期与某些对象的使用联系起来资源。例如,如果您有一个 std::string 作为函数中的局部变量,则该字符串不完全是一个 "contiguous" 对象,而是连接到内存中的两个不同位置。堆栈上有一个固定大小(sizeof(std::string),事实上)的小块,其中包含一个指针和一些其他信息,指向堆上一个动态大小的缓冲区。形式上,只有小的 "control" 部分是 std::string 对象,但从程序员的角度来看,缓冲区也是字符串的 "part" 并且是您通常认为的部分。你不能像这样使用 memcpy 复制一个 std::string 对象——想想如果你有 std::string s 并且你尝试从地址 [=] 复制 sizeof(std::string) 字节会发生什么21=] 得到第二个字符串。您将得到两个控制块,而不是两个不同的字符串对象,每个控制块都指向同一个缓冲区。当第一个被销毁时,那个缓冲区被删除,所以使用第二个会导致段错误,或者当第二个被销毁时,你会得到双重删除。

通常,使用 memcpy 复制重要的 C++ 对象是非法的,并且会导致未定义的行为。这是因为它与 C++ 的核心思想之一相冲突,即对象的创建和销毁可能会产生由程序员使用 ctors 和 dtor 定义的重要后果。对象生命周期可用于创建和强制执行用于推理程序的不变量。 memcpy 是一种仅复制一些字节的 "dumb" 低级方法——它可能会绕过强制执行使程序运行的不变量的机制,这就是如果使用不当会导致未定义行为的原因。

相反,在 C++ 中,我们有复制构造函数,您可以使用它来安全地制作重要对象的副本。您应该以保留对象所需的不变量的方式编写这些。三法则是关于如何实际做到这一点的指南。

C++11 "move semantics" 思想是新的核心语言特性的集合,添加这些特性是为了扩展和改进 C++98 中的传统复制构造机制。具体来说,它是关于我们如何移动潜在的复杂 RAII 对象,而不仅仅是我们已经能够移动的微不足道的对象。我们如何使语言在可能的情况下自动为我们生成移动构造函数等,类似于它为复制构造函数所做的那样。我们如何让它在可以节省时间的情况下使用移动选项,而不会在旧代码中引起错误,或者破坏语言的核心假设。 (这就是为什么我会说你的 intint * 的代码示例与 C++11 移动语义没有什么关系。)

然后,五规则是三规则的相应扩展,它描述了您可能还需要为给定 class 实施移动构造函数/移动赋值运算符而不依赖于语言的默认行为。