我刚听完Scott Meyers关于C++0x的软件工程广播播客采访。大多数新特性对我来说都是有意义的,我现在对C++0x非常兴奋,只有一个例外。我仍然不懂移动语义。。。到底是什么?
当前回答
移动语义是指在没有人需要源值时转移资源,而不是复制资源。
在C++03中,对象经常被复制,只有在任何代码再次使用该值之前才会被销毁或赋值。例如,当您从函数中按值返回时,除非RVO踢入您返回的值,否则该值将被复制到调用方的堆栈帧,然后超出范围并被销毁。这只是众多示例中的一个:当源对象是临时对象时,请参阅passbyvalue;当超过其capacity()时,可以使用排序等算法重新排列项目。
当这种复制/销毁对很昂贵时,通常是因为对象拥有一些重量级资源。例如,vector<string>可能拥有一个动态分配的内存块,其中包含一个字符串对象数组,每个字符串对象都有自己的动态内存。复制这样的对象代价很高:必须为源中每个动态分配的块分配新的内存,并复制所有值。然后需要释放刚才复制的所有内存。然而,移动一个大向量<string>意味着只需将几个指针(指动态内存块)复制到目标,并在源中将它们清零。
其他回答
移动语义是指在没有人需要源值时转移资源,而不是复制资源。
在C++03中,对象经常被复制,只有在任何代码再次使用该值之前才会被销毁或赋值。例如,当您从函数中按值返回时,除非RVO踢入您返回的值,否则该值将被复制到调用方的堆栈帧,然后超出范围并被销毁。这只是众多示例中的一个:当源对象是临时对象时,请参阅passbyvalue;当超过其capacity()时,可以使用排序等算法重新排列项目。
当这种复制/销毁对很昂贵时,通常是因为对象拥有一些重量级资源。例如,vector<string>可能拥有一个动态分配的内存块,其中包含一个字符串对象数组,每个字符串对象都有自己的动态内存。复制这样的对象代价很高:必须为源中每个动态分配的块分配新的内存,并复制所有值。然后需要释放刚才复制的所有内存。然而,移动一个大向量<string>意味着只需将几个指针(指动态内存块)复制到目标,并在源中将它们清零。
你知道复制语义是什么意思吗?这意味着您有可复制的类型,对于用户定义的类型,您可以明确地编写复制构造函数和赋值运算符,或者编译器隐式地生成它们。这将复制一份。
移动语义基本上是一种用户定义的类型,带有构造函数,它接受一个非常量的r值引用(使用&&(是两个&符号)的新类型引用),这称为移动构造函数,赋值运算符也是如此。那么,移动构造函数做什么呢?它不是从源参数复制内存,而是将内存从源“移动”到目标。
你什么时候想这样做?那么std::vector就是一个例子,假设您创建了一个临时std::vector,然后从一个函数返回它,比如:
std::vector<foo> get_foos();
当函数返回时,复制构造函数将产生开销,如果(在C++0x中也是如此)std::vector有一个移动构造函数,而不是复制它。它只需要设置指针并将动态分配的内存“移动”到新实例。这有点像std::auto_ptr的所有权转移语义。
如果你真的对移动语义的一个好的、深入的解释感兴趣,我强烈建议你阅读关于它们的原始论文《向C++语言添加移动语义支持的建议》
这本书非常容易阅读和阅读,为他们提供的好处提供了极好的理由。WG21网站上还有其他关于移动语义的最新论文,但这篇文章可能是最直接的,因为它从顶层的角度处理问题,并没有深入到语言的细节。
简单(实用)地说:
复制对象意味着复制其“静态”成员,并为其动态对象调用新运算符。正确的
class A
{
int i, *p;
public:
A(const A& a) : i(a.i), p(new int(*a.p)) {}
~A() { delete p; }
};
然而,移动一个对象(我重复,从实际的角度来看)意味着只复制动态对象的指针,而不创建新的指针。
但是,这不危险吗?当然,您可以破坏动态对象两次(分段错误)。因此,为了避免这种情况,您应该使源指针“无效”,以避免破坏它们两次:
class A
{
int i, *p;
public:
// Movement of an object inside a copy constructor.
A(const A& a) : i(a.i), p(a.p)
{
a.p = nullptr; // pointer invalidated.
}
~A() { delete p; }
// Deleting NULL, 0 or nullptr (address 0x0) is safe.
};
好的,但是如果我移动一个对象,源对象就会变得无用,不是吗?当然,但在某些情况下,这非常有用。最明显的一点是,当我用匿名对象(时态、右值对象……,你可以用不同的名称调用它)调用函数时:
void heavyFunction(HeavyType());
在这种情况下,将创建一个匿名对象,然后复制到函数参数,然后删除。因此,这里最好移动对象,因为您不需要匿名对象,而且可以节省时间和内存。
这导致了“右值”引用的概念。它们存在于C++11中,只是为了检测接收到的对象是否是匿名的。我想你已经知道“左值”是一个可赋值的实体(=运算符的左边部分),所以你需要一个对象的命名引用来充当左值。右值正好相反,一个没有命名引用的对象。因此,匿名对象和右值是同义词。因此:
class A
{
int i, *p;
public:
// Copy
A(const A& a) : i(a.i), p(new int(*a.p)) {}
// Movement (&& means "rvalue reference to")
A(A&& a) : i(a.i), p(a.p)
{
a.p = nullptr;
}
~A() { delete p; }
};
在这种情况下,当应该“复制”A类型的对象时,编译器会根据传递的对象是否命名来创建左值引用或右值引用。如果没有,将调用移动构造函数,并且您知道该对象是临时的,您可以移动其动态对象而不是复制它们,从而节省空间和内存。
务必记住,“静态”对象总是被复制的。没有办法“移动”静态对象(堆栈中的对象,而不是堆中的对象)。因此,当对象没有动态成员(直接或间接)时,区分“移动”/“复制”是不相关的。
如果您的对象很复杂,并且析构函数具有其他次要影响,例如调用库的函数、调用其他全局函数或其他任何函数,那么最好使用标志来表示移动:
class Heavy
{
bool b_moved;
// staff
public:
A(const A& a) { /* definition */ }
A(A&& a) : // initialization list
{
a.b_moved = true;
}
~A() { if (!b_moved) /* destruct object */ }
};
因此,代码更短(不需要为每个动态成员分配nullptr),更通用。
其他典型问题:A&&和常量A&&之间的区别是什么?当然,在第一种情况下,你可以修改对象,在第二种情况下不是,而是,实际意义?在第二种情况下,您不能修改它,因此您没有办法使对象无效(除了使用可变标志或类似的标志),并且与复制构造函数没有实际区别。
什么是完美的转发?重要的是要知道,“右值引用”是对“调用者范围”中命名对象的引用。但在实际作用域中,右值引用是对象的名称,因此它充当命名对象。如果将右值引用传递给另一个函数,则传递的是一个命名对象,因此该对象不像时间对象那样被接收。
void some_function(A&& a)
{
other_function(a);
}
对象a将被复制到other_function的实际参数。如果希望对象a继续被视为临时对象,则应使用std::move函数:
other_function(std::move(a));
使用这一行,std::move将将a强制转换为右值,other_function将接收该对象作为未命名对象。当然,如果other_function没有特定的重载来处理未命名对象,那么这种区别并不重要。
这是完美的转发吗?不是,但我们很接近。完美转发仅在使用模板时有用,其目的是:如果我需要将一个对象传递给另一个函数,我需要如果我接收到一个命名对象,该对象将作为一个命名的对象传递,如果没有,我希望像未命名的对象一样传递它:
template<typename T>
void some_function(T&& a)
{
other_function(std::forward<T>(a));
}
这是一个使用完美转发的原型函数的签名,在C++11中通过std::forward实现。此函数利用模板实例化的一些规则:
`A& && == A&`
`A&& && == A&&`
因此,如果T是对a的左值引用(T=a&),那么a也是(a&&&=>a&)。如果T是对a的右值引用,则也是(a&&&&&=>a&&)。在这两种情况下,a都是实际作用域中的命名对象,但从调用方作用域的角度来看,T包含其“引用类型”的信息。此信息(T)作为模板参数传递给转发,并根据T的类型移动或不移动“a”。
这就像复制语义,但不必复制所有数据,而是从“移动”的对象中窃取数据。