我刚听完Scott Meyers关于C++0x的软件工程广播播客采访。大多数新特性对我来说都是有意义的,我现在对C++0x非常兴奋,只有一个例外。我仍然不懂移动语义。。。到底是什么?
当前回答
为了说明移动语义的需要,让我们考虑这个没有移动语义的示例:
下面是一个函数,它接受T类型的对象并返回相同类型的对象:
T f(T o) { return o; }
//^^^ new object constructed
上述函数使用值调用,这意味着当调用此函数时,必须构造一个对象以供函数使用。由于函数也按值返回,因此为返回值构造另一个新对象:
T b = f(a);
//^ new object constructed
已经构造了两个新对象,其中一个是仅在函数期间使用的临时对象。
当从返回值创建新对象时,调用复制构造函数将临时对象的内容复制到新对象b。函数完成后,函数中使用的临时对象超出范围并被销毁。
现在,让我们考虑一下复制构造函数的作用。
它必须首先初始化对象,然后将所有相关数据从旧对象复制到新对象。根据类的不同,它可能是一个包含大量数据的容器,那么这可能会占用大量的时间和内存
// Copy constructor
T::T(T &old) {
copy_data(m_a, old.m_a);
copy_data(m_b, old.m_b);
copy_data(m_c, old.m_c);
}
使用移动语义,现在可以通过简单地移动数据而不是复制来减少大部分工作的不愉快。
// Move constructor
T::T(T &&old) noexcept {
m_a = std::move(old.m_a);
m_b = std::move(old.m_b);
m_c = std::move(old.m_c);
}
移动数据涉及将数据与新对象重新关联。而且根本不会复制。
这是通过右值引用完成的。右值引用的工作方式与左值引用非常相似,但有一个重要区别:右值引用可以移动,左值不能移动。
来自cpreference.com:
为了使强异常保证成为可能,用户定义的移动构造函数不应抛出异常。事实上,当需要重新定位容器元素时,标准容器通常依赖std::move_if_noexcept在移动和复制之间进行选择。如果同时提供了复制构造函数和移动构造函数,则重载解析会在参数为右值(prvalue,如无名临时值或xvalue,如std::move的结果)时选择移动构造函数,而在参数为左值(命名对象或返回左值引用的函数/运算符)时选择复制构造函数。如果只提供了复制构造函数,则所有参数类别都会选择它(只要它引用const,因为rvalues可以绑定到const引用),这使得当移动不可用时,复制成为移动的备用方法。在许多情况下,即使移动构造函数会产生可观察到的副作用,也会对其进行优化,请参见复制省略。当构造函数将右值引用作为参数时,它被称为“移动构造函数”。它没有义务移动任何东西,类不需要有要移动的资源,并且“移动构造函数”可能无法移动资源,因为在允许(但可能不合理)的情况下,参数是常量值引用(constT&&)。
其他回答
假设您有一个返回实体对象的函数:
Matrix multiply(const Matrix &a, const Matrix &b);
当您编写这样的代码时:
Matrix r = multiply(a, b);
然后普通的C++编译器将为multiply()的结果创建一个临时对象,调用复制构造函数初始化r,然后销毁临时返回值。C++0x中的移动语义允许调用“移动构造函数”通过复制其内容来初始化r,然后丢弃临时值,而不必销毁它。
如果被复制的对象在堆上分配了额外的内存来存储其内部表示,这一点尤其重要(可能与上面的Matrix示例类似)。复制构造函数必须要么完整复制内部表示,要么在内部使用引用计数和写时复制语义。移动构造函数将不占用堆内存,只复制Matrix对象内的指针。
这就像复制语义,但不必复制所有数据,而是从“移动”的对象中窃取数据。
你知道复制语义是什么意思吗?这意味着您有可复制的类型,对于用户定义的类型,您可以明确地编写复制构造函数和赋值运算符,或者编译器隐式地生成它们。这将复制一份。
移动语义基本上是一种用户定义的类型,带有构造函数,它接受一个非常量的r值引用(使用&&(是两个&符号)的新类型引用),这称为移动构造函数,赋值运算符也是如此。那么,移动构造函数做什么呢?它不是从源参数复制内存,而是将内存从源“移动”到目标。
你什么时候想这样做?那么std::vector就是一个例子,假设您创建了一个临时std::vector,然后从一个函数返回它,比如:
std::vector<foo> get_foos();
当函数返回时,复制构造函数将产生开销,如果(在C++0x中也是如此)std::vector有一个移动构造函数,而不是复制它。它只需要设置指针并将动态分配的内存“移动”到新实例。这有点像std::auto_ptr的所有权转移语义。
如果你真的对移动语义的一个好的、深入的解释感兴趣,我强烈建议你阅读关于它们的原始论文《向C++语言添加移动语义支持的建议》
这本书非常容易阅读和阅读,为他们提供的好处提供了极好的理由。WG21网站上还有其他关于移动语义的最新论文,但这篇文章可能是最直接的,因为它从顶层的角度处理问题,并没有深入到语言的细节。
我写这个是为了确保我能正确理解。
创建移动语义是为了避免不必要地复制大型对象。Bjarne Stroustrup在其著作《C++编程语言》中使用了两个默认情况下发生不必要复制的示例:一个是交换两个大对象,另一个是从方法返回一个大对象。
交换两个大对象通常涉及将第一个对象复制到临时对象,将第二个对象复制给第一个对象,以及将临时对象复制到第二个。对于内置类型,这非常快,但对于大型对象,这三个副本可能需要大量时间。“移动赋值”允许程序员重写默认的复制行为,而是交换对对象的引用,这意味着根本没有复制,而且交换操作更快。可以通过调用std::move()方法来调用移动赋值。
默认情况下,从方法返回对象涉及在调用方可访问的位置复制本地对象及其关联数据(因为调用方无法访问本地对象,并且在方法完成时会消失)。当返回内置类型时,此操作非常快,但如果返回大型对象,则可能需要很长时间。移动构造函数允许程序员重写此默认行为,并通过将返回给调用者的对象指向与本地对象关联的堆数据来“重用”与本地对象相关的堆数据。因此不需要复制。
在不允许创建本地对象(即堆栈上的对象)的语言中,这些类型的问题不会发生,因为所有对象都分配在堆上,并且总是通过引用访问。