Answers:
移动语义为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,否则会出现所有权问题。尽可能清楚地了解代码的目的是值得的,因此,移动构造器可能是最好的方法。
当返回值时,移动语义并不一定会带来很大的改进。当/如果使用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中,可以期望它们会被移动。从本质上讲,这通常使我们可以进行浅表复制,而不是(通常要慢得多)进行深表复制。换句话说,使用字符串或向量(仅用于几个示例),我们仅将指针复制到对象中,而不是复制这些指针所引用的所有数据。
考虑:
vector<string> v;
将字符串添加到v时,它将根据需要扩展,并且在每次重新分配时都必须复制字符串。对于move构造函数,这基本上不是问题。
当然,您也可以执行以下操作:
vector<unique_ptr<string>> v;
但这仅能很好地起作用,因为std::unique_ptr
实现了move构造函数。
使用std::shared_ptr
品牌只有当你真正有共同所有权(罕见)的情况下检测。
string
我们有一个Foo
包含30个数据成员的实例怎么办?该unique_ptr
版本将不会是更有效?
shared_ptr
为了快速复制,上帝提防),并且如果移动语义可以在几乎没有编码,语义和整洁度惩罚的情况下实现相同的效果。