C ++中move构造函数的动机和使用


17

最近,我一直在阅读有关C ++中的move构造函数的信息(请参见例如此处),我试图了解它们的工作方式以及何时使用它们。

据我了解,移动构造函数用于缓解由于复制大型对象而导致的性能问题。维基百科页面上说:“ C ++ 03的一个长期性能问题是,当对象按值传递时,可能隐式地发生昂贵且不必要的深拷贝。”

我通常会解决这种情况

  • 通过引用传递对象,或
  • 通过使用智能指针(例如boost :: shared_ptr)来传递对象(智能指针而不是对象被复制)。

在哪种情况下上述两种技术还不够,而使用move构造函数更方便?


1
除了移动语义可以实现更多的事实(如答案中所述)外,您不应该问在什么情况下通过引用或通过智能指针进行传递是不够的,但是这些技术是否真的是最好,最简洁的方法这样做(shared_ptr为了快速复制,上帝提防),并且如果移动语义可以在几乎没有编码,语义和整洁度惩罚的情况下实现相同的效果。
克里斯说,恢复莫妮卡(Monica)2012年

Answers:


16

移动语义为C ++引入了一个完整的维度-不仅仅是为了让您廉价地返回值而已。

例如,没有move-semantics std::unique_ptr不起作用-请看一下std::auto_ptr,因为move-semantics的引入已弃用了,并在C ++ 17中将其删除。移动资源与复制资源有很大不同。它允许转移唯一项目的所有权。

例如,让我们不要std::unique_ptr讨论,因为已经对其进行了很好的讨论。让我们看一下OpenGL中的“顶点缓冲对象”。顶点缓冲区表示GPU上的内存-需要使用特殊功能对其进行分配和释放,这可能对其生存时间有严格的限制。同样重要的是只有一个所有者可以使用它。

class vertex_buffer_object
{
    vertex_buffer_object(size_t size)
    {
        this->vbo_handle = create_buffer(..., size);
    }

    ~vertex_buffer_object()
    {
        release_buffer(vbo_handle);
    }
};

void create_and_use()
{
    vertex_buffer_object vbo = vertex_buffer_object(SIZE);

    do_init(vbo); //send reference, do not transfer ownership

    renderer.add(std::move(vbo)); //transfer ownership to renderer
}

现在,可以使用std::shared_ptr- 来完成此操作,但是不要共享此资源。这使使用共享指针变得混乱。您可以使用std::unique_ptr,但这仍然需要移动语义。

显然,我没有实现move构造函数,但是您明白了。

这里相关的事情是某些资源不可复制。您可以传递指针而不是移动指针,但是除非您使用unique_ptr,否则会出现所有权问题。尽可能清楚地了解代码的目的是值得的,因此,移动构造器可能是最好的方法。


感谢您的回答。如果在这里使用共享指针会发生什么?
乔治

我尝试回答一下自己:使用共享指针将不允许控制对象的生存期,而要求对象只能生存一定的时间。
乔治

3
@Giorgio您可以使用共享指针,但这在语义上是错误的。不可能共享缓冲区。而且,从本质上讲,这会使您将指针传递给指针(因为vbo基本上是指向GPU内存的唯一指针)。稍后查看您的代码的人可能会想:“为什么这里有共享的指针?它是共享资源吗?那可能是一个错误!最好尽可能清楚其原始意图是什么。
最多

@Giorgio是的,这也是要求的一部分。在这种情况下,如果“渲染器”想要分配一些资源(可能没有足够的内存用于GPU上的新对象),则该内存必须没有其他任何句柄。如果您不将其传递到其他地方,则使用超出范围的shared_ptr可以工作,但是为什么不可以使其完全变得明显呢?
最多

@Giorgio请参阅我的编辑以进一步澄清。
2012年

5

当返回值时,移动语义并不一定会带来很大的改进。当/如果使用shared_ptr(或类似的东西),您可能过早地悲观了。实际上,几乎所有合理的现代编译器都执行所谓的返回值优化(RVO)和命名返回值优化(NRVO)。这意味着,当你返回一个值,而不是实际的值复制到所有,它们只是将一个隐藏的指针/引用传递到返回之后将要分配值的位置,然后函数使用该指针/引用来创建将要终止的值。C ++标准包括允许这样做的特殊规定,因此,即使(例如)您的复制构造函数具有明显的副作用,也不需要使用复制构造函数返回该值。例如:

#include <vector>
#include <numeric>
#include <iostream>
#include <stdlib.h>
#include <algorithm>
#include <iterator>

class X {
    std::vector<int> a;
public:
    X() {
        std::generate_n(std::back_inserter(a), 32767, ::rand);
    }

    X(X const &x) {
        a = x.a;
        std::cout << "Copy ctor invoked\n";
    }

    int sum() { return std::accumulate(a.begin(), a.end(), 0); }
};

X func() {
    return X();
}

int main() {
    X x = func();

    std::cout << "sum = " << x.sum();
    return 0;
};

这里的基本思想很简单:创建一个具有足够内容的类,如果可能的话,我们宁愿避免复制它(std::vector我们填充了32767个随机整数)。我们有一个明确的副本ctor,它将在/是否被复制时向我们显示。我们还有更多代码来处理对象中的随机值,因此优化器不会(至少很容易)消除类的所有内容,因为它什么也不做。

然后,我们有一些代码从函数中返回这些对象之一,然后使用求和来确保确实创建了对象,而不仅仅是完全忽略了该对象。当我们运行它时,至少在大多数最新/现代的编译器中,我们发现我们编写的副本构造函数根本不会运行-是的,我敢肯定,即使使用a进行快速复制shared_ptr也比不进行复制慢完全没有

移动使您可以做很多事情(如果没有这些事情,直接做)。考虑外部合并排序的“合并”部分-例如,您有8个文件要合并在一起。理想情况下,您希望将所有8个文件放到一个vector-中,但是由于vector(从C ++ 03开始)需要能够复制元素,而ifstream不能复制s,因此您会陷入一些unique_ptr/ shared_ptr,或该命令上的某些内容,以便将它们放入向量中。请注意,即使(例如),我们reserve的空间vector,所以我们要确保我们的ifstream旨意从来没有真正被复制,编译器将不知道,这样的代码将无法编译,即使我们知道拷贝构造函数永远不会反正用过。

即使仍然无法复制,在C ++ 11中ifstream 可以移动。在这种情况下,对象可能不会永远被移动,但事实上,他们可能是,如果有必要保持编译快乐,所以我们可以把我们的ifstream对象中vector直接,没有任何智能指针黑客。

确实可以扩展的向量是一个很好的例子,说明了移动语义确实可能是有用的。在这种情况下,RVO / NRVO将无济于事,因为我们不处理函数(或非常相似的东西)的返回值。我们有一个向量来容纳一些对象,并且我们想将这些对象移动到更大的新内存中。

在C ++ 03中,这是通过在新内存中创建对象的副本,然后销毁旧内存中的旧对象来完成的。但是,制作所有这些副本只是为了扔掉旧副本,这是浪费时间。在C ++ 11中,可以期望它们会被移动。从本质上讲,这通常使我们可以进行浅表复制,而不是(通常要慢得多)进行深表复制。换句话说,使用字符串或向量(仅用于几个示例),我们仅将指针复制到对象中,而不是复制这些指针所引用的所有数据。


感谢您的详细解释。如果我正确理解的话,所有移动起作用的情况都可以由普通的指针处理,但是每次编程所有杂耍的指针都是不安全的(复杂且容易出错)。因此,相反,引擎盖下有一些unique_ptr(或类似的机制),而移动语义可确保在一天结束时仅进行一些指针复制,而没有对象复制。
Giorgio 2012年

@乔治:是的,这是非常正确的。该语言并没有真正添加移动语义。它添加右值引用。一个右值引用(很明显)可以绑定到一个右值,在这种情况下,您知道“窃取”数据的内部表示是安全的,只复制其指针而不是进行深层复制即可。
杰里·科芬

4

考虑:

vector<string> v;

将字符串添加到v时,它将根据需要扩展,并且在每次重新分配时都必须复制字符串。对于move构造函数,这基本上不是问题。

当然,您也可以执行以下操作:

vector<unique_ptr<string>> v;

但这仅能很好地起作用,因为std::unique_ptr实现了move构造函数。

使用std::shared_ptr品牌只有当你真正有共同所有权(罕见)的情况下检测。


但是,如果不是string我们有一个Foo包含30个数据成员的实例怎么办?该unique_ptr版本将不会是更有效?
瓦西里斯(Vassilis)'18 / 12/29

2

返回值是我最常希望按值传递的位置,而不是某种引用。能够快速将对象“返回到堆栈上”而不会造成很大的性能损失,这将是很好的。另一方面,解决这个问题并不是特别困难(共享指针是如此易于使用...),所以我不确定仅是能够做到这一点就值得对我的对象进行额外的工作。


我通常也使用智能指针来包装从函数/方法返回的对象。
乔治

1
@Giorgio:确实既令人困惑又缓慢。
DeadMG

如果返回一个简单的On-the-stack对象,所以没有需要共享的师生比等现代编译器应该执行一个自动移动
基督教塞韦林
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.