-1

I've been trying to nail down the rule of 5, but most of the information online is vastly over-complicated, and the example codes differ.

Even my textbook doesn't cover this topic very well.

On move semantics:

Templates, rvalues and lvalues aside, as I understand it, move semantics are simply this:

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

As apposed to copying, which would be the equivalent of something like this:

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'

No explanation of rvalues, lvalues or templates is necessary, I would go as far as to say those topics are unrelated.

The fact that the first example is faster than the second, should be a given. And I would also point out that any efficient code prior to C++ 11 will do this.

To my understanding, the idea was to bundle all of this behavior in a neat little operator move() in std library.

When writing copy constructors and copy assignment operators, I simply do this:

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;
}

The equivalent, one would think, would be 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);
}

I'm quite certain this won't work, so my question is: How do you achieve this type of constructor functionality with move semantics?

4

1 回答 1

0

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

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

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

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


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

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

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

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

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

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

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

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

C++11 的“移动语义”思想是一组新的核心语言特性,这些特性是为了扩展和改进 C++98 的传统复制构造机制而添加的。具体来说,它是关于我们如何移动可能复杂的 RAII 对象,而不仅仅是我们已经能够移动的琐碎对象。我们如何让语言在可能的情况下自动为我们生成移动构造函数等,就像它为复制构造函数所做的那样。我们如何让它在可以节省时间的情况下使用移动选项,而不会导致旧代码中的错误或破坏语言的核心假设。(这就是为什么我会说带有int's 和int *'s 的代码示例与 C++11 移动语义几乎没有关系。)

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

于 2016-02-10T05:27:56.070 回答