我刚听完Scott Meyers关于C++0x的软件工程广播播客采访。大多数新特性对我来说都是有意义的,我现在对C++0x非常兴奋,只有一个例外。我仍然不懂移动语义。。。到底是什么?
当前回答
我写这个是为了确保我能正确理解。
创建移动语义是为了避免不必要地复制大型对象。Bjarne Stroustrup在其著作《C++编程语言》中使用了两个默认情况下发生不必要复制的示例:一个是交换两个大对象,另一个是从方法返回一个大对象。
交换两个大对象通常涉及将第一个对象复制到临时对象,将第二个对象复制给第一个对象,以及将临时对象复制到第二个。对于内置类型,这非常快,但对于大型对象,这三个副本可能需要大量时间。“移动赋值”允许程序员重写默认的复制行为,而是交换对对象的引用,这意味着根本没有复制,而且交换操作更快。可以通过调用std::move()方法来调用移动赋值。
默认情况下,从方法返回对象涉及在调用方可访问的位置复制本地对象及其关联数据(因为调用方无法访问本地对象,并且在方法完成时会消失)。当返回内置类型时,此操作非常快,但如果返回大型对象,则可能需要很长时间。移动构造函数允许程序员重写此默认行为,并通过将返回给调用者的对象指向与本地对象关联的堆数据来“重用”与本地对象相关的堆数据。因此不需要复制。
在不允许创建本地对象(即堆栈上的对象)的语言中,这些类型的问题不会发生,因为所有对象都分配在堆上,并且总是通过引用访问。
其他回答
你知道复制语义是什么意思吗?这意味着您有可复制的类型,对于用户定义的类型,您可以明确地编写复制构造函数和赋值运算符,或者编译器隐式地生成它们。这将复制一份。
移动语义基本上是一种用户定义的类型,带有构造函数,它接受一个非常量的r值引用(使用&&(是两个&符号)的新类型引用),这称为移动构造函数,赋值运算符也是如此。那么,移动构造函数做什么呢?它不是从源参数复制内存,而是将内存从源“移动”到目标。
你什么时候想这样做?那么std::vector就是一个例子,假设您创建了一个临时std::vector,然后从一个函数返回它,比如:
std::vector<foo> get_foos();
当函数返回时,复制构造函数将产生开销,如果(在C++0x中也是如此)std::vector有一个移动构造函数,而不是复制它。它只需要设置指针并将动态分配的内存“移动”到新实例。这有点像std::auto_ptr的所有权转移语义。
假设您有一个返回实体对象的函数:
Matrix multiply(const Matrix &a, const Matrix &b);
当您编写这样的代码时:
Matrix r = multiply(a, b);
然后普通的C++编译器将为multiply()的结果创建一个临时对象,调用复制构造函数初始化r,然后销毁临时返回值。C++0x中的移动语义允许调用“移动构造函数”通过复制其内容来初始化r,然后丢弃临时值,而不必销毁它。
如果被复制的对象在堆上分配了额外的内存来存储其内部表示,这一点尤其重要(可能与上面的Matrix示例类似)。复制构造函数必须要么完整复制内部表示,要么在内部使用引用计数和写时复制语义。移动构造函数将不占用堆内存,只复制Matrix对象内的指针。
我写这个是为了确保我能正确理解。
创建移动语义是为了避免不必要地复制大型对象。Bjarne Stroustrup在其著作《C++编程语言》中使用了两个默认情况下发生不必要复制的示例:一个是交换两个大对象,另一个是从方法返回一个大对象。
交换两个大对象通常涉及将第一个对象复制到临时对象,将第二个对象复制给第一个对象,以及将临时对象复制到第二个。对于内置类型,这非常快,但对于大型对象,这三个副本可能需要大量时间。“移动赋值”允许程序员重写默认的复制行为,而是交换对对象的引用,这意味着根本没有复制,而且交换操作更快。可以通过调用std::move()方法来调用移动赋值。
默认情况下,从方法返回对象涉及在调用方可访问的位置复制本地对象及其关联数据(因为调用方无法访问本地对象,并且在方法完成时会消失)。当返回内置类型时,此操作非常快,但如果返回大型对象,则可能需要很长时间。移动构造函数允许程序员重写此默认行为,并通过将返回给调用者的对象指向与本地对象关联的堆数据来“重用”与本地对象相关的堆数据。因此不需要复制。
在不允许创建本地对象(即堆栈上的对象)的语言中,这些类型的问题不会发生,因为所有对象都分配在堆上,并且总是通过引用访问。
这是Bjarne Stroustrup的《C++编程语言》一书中的答案。如果你不想看视频,可以看下面的文字:
考虑一下这个片段。从运算符+返回涉及将结果从局部变量res复制到调用者可以访问的地方。
Vector operator+(const Vector& a, const Vector& b)
{
if (a.size()!=b.size())
throw Vector_siz e_mismatch{};
Vector res(a.size());
for (int i=0; i!=a.size(); ++i)
res[i]=a[i]+b[i];
return res;
}
我们真的不想要副本;我们只是想从函数中得到结果。所以我们需要移动Vector而不是复制它。我们可以如下定义移动构造函数:
class Vector {
// ...
Vector(const Vector& a); // copy constructor
Vector& operator=(const Vector& a); // copy assignment
Vector(Vector&& a); // move constructor
Vector& operator=(Vector&& a); // move assignment
};
Vector::Vector(Vector&& a)
:elem{a.elem}, // "grab the elements" from a
sz{a.sz}
{
a.elem = nullptr; // now a has no elements
a.sz = 0;
}
&&表示“右值引用”,是我们可以绑定右值的引用。“rvalue”'旨在补充“lvalue”,它大致意思是“可以出现在赋值左侧的值”。因此,rvalue大致意思是:“不能赋值的值”,例如函数调用返回的整数,以及Vectors运算符+()中的res局部变量。
现在,语句返回res;不会复制!
我发现用示例代码来理解移动语义是最容易的。让我们从一个非常简单的字符串类开始,它只包含一个指向堆分配的内存块的指针:
#include <cstring>
#include <algorithm>
class string
{
char* data;
public:
string(const char* p)
{
size_t size = std::strlen(p) + 1;
data = new char[size];
std::memcpy(data, p, size);
}
既然我们选择自己管理记忆,我们就需要遵循三个法则。我将推迟编写赋值运算符,现在只实现析构函数和复制构造函数:
~string()
{
delete[] data;
}
string(const string& that)
{
size_t size = std::strlen(that.data) + 1;
data = new char[size];
std::memcpy(data, that.data, size);
}
复制构造函数定义了复制字符串对象的含义。参数const string&绑定到string类型的所有表达式,允许您在以下示例中创建副本:
string a(x); // Line 1
string b(x + y); // Line 2
string c(some_function_returning_a_string()); // Line 3
现在是对移动语义的关键洞察。注意,只有在我们复制x的第一行中,这个深度复制才是真正必要的,因为我们可能希望稍后检查x,如果x发生了某种变化,我们会非常惊讶。你注意到我刚才说了三次x(如果你包括这句话的话是四次),每次都是指同一个对象吗?我们称x等表达式为“lvalues”。
第2行和第3行中的参数不是lvalues,而是rvalues,因为基础字符串对象没有名称,因此客户端无法在稍后的时间点再次检查它们。右值表示在下一个分号处销毁的临时对象(更准确地说:在词汇上包含右值的完整表达式的末尾)。这一点很重要,因为在b和c的初始化过程中,我们可以对源字符串做任何我们想做的事情,而客户端无法分辨出区别!
C++0x引入了一种称为“右值引用”的新机制,允许我们通过函数重载检测右值参数。我们所要做的就是编写一个带有右值引用参数的构造函数。在该构造函数中,我们可以对源代码做任何我们想做的事情,只要我们让它处于某种有效状态:
string(string&& that) // string&& is an rvalue reference to a string
{
data = that.data;
that.data = nullptr;
}
我们在这里做了什么?我们没有深度复制堆数据,而是复制了指针,然后将原始指针设置为null(以防止源对象的析构函数中的“delete[]”释放我们的“刚刚窃取的数据”)。实际上,我们已经“窃取”了最初属于源字符串的数据。同样,关键的见解是,在任何情况下,客户端都无法检测到源代码已被修改。因为我们没有在这里真正复制,所以我们称这个构造函数为“移动构造函数”。它的工作是将资源从一个对象移动到另一个对象,而不是复制它们。
恭喜您,您现在了解了移动语义的基础知识!让我们继续实现赋值运算符。如果您不熟悉复制和交换习惯用法,请学习它并回来,因为它是一个与异常安全相关的很棒的C++习惯用法。
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
};
嗯,就这样?“右值参考值在哪里?”你可能会问。“我们这里不需要它!”我的回答是:)
请注意,我们通过值传递参数,因此它必须像其他字符串对象一样进行初始化。具体如何初始化?在C++98的早期,答案应该是“复制构造函数”。在C++0x中,编译器根据赋值运算符的参数是左值还是右值,在复制构造函数和移动构造函数之间进行选择。
因此,如果你说a=b,复制构造函数将初始化它(因为表达式b是一个左值),赋值运算符将用新创建的深度副本交换内容。这就是复制和交换习惯用法的定义——制作一个副本,用副本交换内容,然后通过离开范围来删除副本。这里没有什么新鲜事。
但是如果你说a=x+y,move构造函数将初始化它(因为表达式x+y是一个右值),所以不涉及深度复制,只涉及有效的移动。这仍然是一个独立于论点的对象,但它的构造微不足道,因为堆数据不必复制,只需移动即可。不需要复制它,因为x+y是一个右值,同样,可以从右值表示的字符串对象中移动。
总之,复制构造函数进行深度复制,因为源必须保持不变。另一方面,move构造函数可以复制指针,然后将源中的指针设置为null。以这种方式“取消”源对象是可以的,因为客户端无法再次检查对象。
我希望这个例子能传达出要点。为了保持简单,我特意省略了很多关于重新赋值引用和移动语义的内容。如果你想了解更多详情,请参阅我的补充答案。
推荐文章
- Uint8_t不能用cout打印
- c++中不必要的花括号
- 如何反转一个c++向量?
- 如何开始开发Internet Explorer扩展?
- Std::auto_ptr改为Std::unique_ptr
- int的最大值
- c++中有最大数组长度限制吗?
- 什么是“参数依赖查找”(又名ADL,或“Koenig查找”)?
- 公共朋友交换成员函数
- 如何在Go中使用c++
- 自定义c++分配器的引人注目的例子?
- RAII和c++中的智能指针
- 如何构建和使用谷歌TensorFlow c++ api
- 断言是邪恶的吗?
- 下面这些短语在c++中是什么意思:0 -,default-和value-initialization?