【问题标题】:Copy Constructors and Move Semantics by Following Objects通过跟随对象复制构造函数和移动语义
【发布时间】:2015-02-09 12:33:42
【问题描述】:

我已经阅读了无数关于复制构造函数和移动语义的文章。我觉得我“有点”明白发生了什么,但很多解释都忽略了幕后实际发生的事情(这就是让我感到困惑的原因)。

例如:

string b(x + y);

string(string&& that)
    {
        data = that.data;
        that.data = 0;
    }

对象在内存中实际发生了什么?所以你有一些对象'b' 接受x + y 这是一个右值,然后调用移动构造函数。这真的让我感到困惑......为什么要这样做?

我知道这样做的好处是“移动”数据而不是复制它,但我在这里迷失的地方是当我尝试将每个对象/参数在内存级别上发生的事情拼凑起来时。

对不起,如果这听起来令人困惑,谈论它甚至让我自己感到困惑。

编辑:

总之,我理解复制构造函数和移动构造函数的“为什么”......我只是不明白“如何”。

【问题讨论】:

  • 在内存中:将that.data 的值复制到this->data 并将that.data 设置为0。没有“幕后”。
  • IDK 如果这会有所帮助,但 copy 可能是:char *b = new char[strlen(a)+1]; strcpy(b, a); delete[] a; a = nullptr;,而 move 将是 char *b = a; a = nullptr;。显然后者的分配更少,所以只要你想要这些语义,它就更好了。

标签: c++ c++11


【解决方案1】:

(x + y) 给你一个字符串值。您想将其存储在b 中而不复制它。早在 C++11 和移动语义之前,Return Value Optimization (RVO) 就实现了这一点。

【讨论】:

  • 所以'that' 指的是'x+y' 并且它的值被用来创建一个'b' 对象而不复制数据? x+y 右值发生了什么?指针是如何复制的?右值是在最后一个卷曲处消失的临时对象吗?
  • 我还应该注意,这些优化在 c++98 中是不保证的。
【解决方案2】:

复杂的对象通常不会完全基于堆栈。让我们举个例子:

class String {
public:
  // happy fun API
private:
  size_t size;
  char* data;
};

像大多数字符串一样,我们的字符串是一个字符数组。它本质上是一个保持字符数组和适当大小的对象。

如果是副本,则涉及两个步骤。首先复制size,然后复制data。但是data 只是一个指针。所以如果我们复制对象然后修改原来的,两个地方都指向同一个数据,我们的复制就改变了。这不是我们想要的。

因此,必须做的就是做我们第一次制作对象时所做的相同的事情,newdata 到适当的大小。

因此,当我们复制对象时,我们需要执行以下操作:

String::String(String const& copy) {
  size = copy.size;
  data = new int[size];
  memcpy(data, copy.data, size);
}

但另一方面,如果我们只需要移动数据,我们可以这样做:

String::String(String&& copy) {
  size = copy.size;
  data = copy.data;
  copy.size = 0;
  copy.data = nullptr; // So copy's dtor doesn't try to free our data.
}

现在在幕后,指针只是有点...传递给我们。我们不必分配更多信息。这就是为什么移动是首选的原因。在堆上分配和复制内存可能是一项非常昂贵的操作,因为它不是在堆栈本地发生,而是在其他地方发生,因此必须获取内存,它可能不在缓存中,等等。

【讨论】:

  • 这太完美了。我现在完全明白了...感谢您抽出时间来布置它。这个概念对我来说相当新。
【解决方案3】:
... (x + y);

让我们假设 Short-String-Optimisation 没有起作用——要么是因为字符串实现不使用它,要么是字符串值太长。 operator+ 按值返回,因此必须使用与 xy 字符串完全无关的新缓冲区创建一个临时...

[ string { const char* _p_data; ... } ]
                           \
                            \-------------------------(heap)--------[ "hello world!" ];

没有优化,这是为 string 构造函数准备参数 - “之前”考虑到该构造函数将如何处理参数。

string b(x + y);

这里调用了string(string&&) 构造函数,因为编译器知道上面的临时变量适合从中移动。当构造函数开始运行时,它指向文本的指针未初始化 - 如下图所示,再次为上下文显示临时:

[ string { const char* _p_data; ... } ]
                           \
                            \-------------------------(heap)--------[ "hello world!" ];


[ string b { const char* _p_data; ... } ]
                           \
                            \----? uninitialised

b 的移动构造函数所做的就是从临时对象中窃取现有的堆缓冲区。

                          nullptr
                         /
[ string { const char* _p_data; ... } ]

                             -------------------------(heap)--------[ "hello world!" ];
                            /
                           /
[ string b { const char* _p_data; ... } ]

它还需要将临时的_p_data 设置为nullptr 以确保当临时的析构函数运行时它不会delete[] 现在认为由b 拥有的缓冲区。 (移动构造函数也会“移动”其他数据成员——“容量”值,指向“结束”位置的指针或“大小”值等)。

所有这一切都避免了b 的构造函数创建第二个堆缓冲区,将所有文本复制到其中,然后对delete[] 临时缓冲区做额外的工作。

【讨论】:

  • 太棒了。谢谢。
猜你喜欢
  • 2013-11-05
  • 1970-01-01
  • 2011-01-20
  • 2018-08-27
  • 2023-03-28
  • 2023-03-20
  • 1970-01-01
  • 2017-09-11
  • 2018-09-23
相关资源
最近更新 更多