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

Move assignment operator, move constructor

提问人:bigcodeszzer 提问时间:2/9/2016 最后编辑:bigcodeszzer 更新时间:2/10/2016 访问量:1164

问:

我一直在试图确定 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'

不需要解释 rvalues、lvalues 或模板,我甚至可以说这些主题是无关的。

第一个例子比第二个例子快,这一事实应该是给定的。我还要指出,在 C++ 11 之前,任何有效的代码都会这样做。

据我了解,这个想法是将所有这些行为捆绑在 std 库中一个简洁的小运算符 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);
}

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

C++11 语义 移动 构造函数 法则 move-assignment-operator

评论

0赞 Mat 2/9/2016
你的两个“移动”示例(前两行代码)不会编译(第一行)也不会移动任何东西(第一行和第二行)......
0赞 Neijwiert 2/9/2016
移动语义学背后的原理是,您可以有效地将数据移动到其他地方,而无需复制。如果无法从程序中访问数据的先前所有者(未命名变量),则可以在没有未定义行为的情况下执行此操作
0赞 bigcodeszzer 2/9/2016
@Neijwiert 很公平,但是对于移动构造函数和移动赋值运算符,仍然可以联系到以前的所有者。
1赞 Mat 2/9/2016
不,它没有修复,仍然崩溃,您正在取消引用 pointer2 而没有初始化它。即使你想写的就是评论所说的,这仍然不会在任何地方移动任何东西。
1赞 Peter - Reinstate Monica 2/9/2016
而你的搬家中缺少的关键部分:一个简单的任务。目标中没有重新分配和深度复制,而不是非移动复制控制。data = movefrom.data

答:

0赞 Chris Beck 2/10/2016 #1

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

从概念上讲,短语“移动语义”在C++中是什么意思吗?

是“如何编写移动 ctor 和移动赋值运算符? ?

这是我试图介绍这个概念的尝试。如果要查看代码示例,请查看注释中链接的任何其他 SO 问题。


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

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

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

第三种选择是移动对象而不是复制它。移动对象时,它不会被复制,而是在新网站中以独占方式提供,而不再在旧网站中可用。显然,只有当您在旧站点不再需要它时,您才能这样做,但在这种情况下,这会为您节省一份副本,从而节省大量资金。

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

但是,在现代 C++ 中,许多对象都不是微不足道的——它们可能“拥有”对堆内存的引用,并使用 RAII 管理此内存,RAII 将对象的生存期与某些资源的使用联系起来。例如,如果你在一个函数中有一个局部变量,那么字符串并不完全是一个“连续”的对象,而是连接到内存中的两个不同位置。堆栈上有一个固定大小(实际上是)的小块,其中包含一个指针和一些其他信息,指向堆上动态大小的缓冲区。从形式上讲,只有小的“控制”部分是对象,但从程序员的角度来看,缓冲区也是字符串的“部分”,是你通常想到的部分。你不能使用这样的对象来复制 -- 想想如果你有,你尝试从地址复制字节来获得第二个字符串会发生什么。最终将得到两个控制块,而不是两个不同的字符串对象,每个控制块都指向同一个缓冲区。当第一个缓冲区被销毁时,该缓冲区将被删除,因此使用第二个缓冲区将导致段错误,或者当第二个缓冲区被销毁时,您将获得双重删除。std::stringsizeof(std::string)std::stringstd::stringmemcpystd::string ssizeof(std::string)&s

通常,复制非平凡的 C++ 对象是非法的,并会导致未定义的行为。这是因为它与 C++ 的核心思想之一相冲突,即对象的创建和销毁可能会产生由程序员使用 ctor 和 dtors 定义的非平凡后果。对象生存期可用于创建和强制执行不变量,您可以使用这些变量来推理程序。 是一种“愚蠢”的低级方法,只复制一些字节 - 它可能绕过强制执行使程序工作的不变量的机制,这就是为什么如果使用不当,它会导致未定义的行为。memcpymemcpy

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

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

因此,五法则是三法则的相应扩展,它描述了您可能还需要为给定类实现移动 ctor / 移动赋值运算符并且不依赖于语言的默认行为的条件。

评论

0赞 bigcodeszzer 2/12/2016
我的问题是:你怎么写它们。堆栈问题、在线信息和我的教科书之间存在差异。
0赞 bigcodeszzer 2/12/2016
我所说的它们指的是移动运算符和移动构造函数。
0赞 bigcodeszzer 2/12/2016
至于悬空指针的事情,我可能只是不明白这将是一个问题的情况。也就是说,即使是移动语义也会将源指针转换为空,因此这样做应该可以消除该问题。就稳定性而言,我绝不会在没有先测试动态块是否为 null 的情况下引用它。所以我真的不明白你的意思?
0赞 Chris Beck 2/12/2016
那么,你如何准确地编写它们取决于所持有的资源,类具有哪些不变量,等等。主要思想是 1.如果 A 是从 B 构造/移动分配的,那么 A 应该在您认为重要的所有方面都与 B 相同。类似于你有复制构造它。2. 您可以以任何方式修改 B,但您必须将其保持在“有效状态”。B 的生存期在移动后不会立即结束,它的 dtor 仍会在以后调用,如果这会导致崩溃,那么您的移动操作是有缺陷的。
0赞 Chris Beck 2/12/2016
我想一种思考方式是,首先采取你的复制 ctor 实现,你已经知道如何正确地做到这一点。现在想想,如果我知道可以从输入中窃取资源而不是制作副本,我怎么能更快地做同样的事情。如果有优化机会,那么你可以以这种方式对移动构造函数进行编码,并在你的程序中获得加速。这就是所有正在发生的事情。其实和想着,我该如何实现复制ctor是一样的,只是你要不辜负的后备条件是宽松的。