这是关于C++中的高效值类型的系列文章中的第三篇。在上一篇中,我们介绍了C++0x的右值引用,描述了如何建立一个可转移类型,并示范了如何显式地利用可转移性。现在我们来看看转移优化的其它一些机会,开拓一些关于转移方面的新领域。
在开始讨论进一步的优化之前,我们要先了解,一个匿名的右值引用是右值,而一个命名的右值引用则是左值。我把它写下来以便你记得更清楚:
切记:一个命名的右值引用是左值。
我承认这有点不合情理,但是请看以下例子:
int g(X const&); // logically non-mutating
int g(X&&); // ditto, but moves from rvalues
int f(X&& a)
{
g(a);
g(a);
}
如果我们把 f 中的 a 视为右值,那么第一次对 g 的调用将会从 a 进行转移,第二次调用将会看到一个被修改过的 a。这不仅是反直觉的;而且它违反了一个保证,即调用 g 不会有可见的对任何东西的修改。所以,一个命名的右值引用是和其它引用一样的,只有匿名的右值引用才被特殊对待。为了给第二次的 g 调用一个机会进行转移,我们必须如下重写:
#include // for std::move
int f(X&& a)
{
g(a);
g( std::move(a) );
}
要记得 std::move 本身并不进行转移。它只是将其参数转换为匿名右值引用,这样接下来就可以进行转移。
转移语义对于优化二元操作符的使用尤其有用。考虑以下代码:
class Matrix
{
…
std::vector storage;
};
Matrix operator+(Matrix const& left, Matrix const& right)
{
Matrix result(left);
result += right; // delegates to +=
return result;
}
Matrix a, b, c, d;
…
Matrix x = a + b + c + d;
每一次调用 operator+ 时,都要执行 Matrix 的复制构造函数来创建 result。因此,即使RVO可以消除返回时对 result 的复制,上述表达式还要是进行三次 Matrix 的复制(每个 + 各一次),每次都要构造一个很大的 vector。复制省略可以让这些 result 矩阵中的一个与 x 成为同一个对象,但是另外两个还是要被销毁的,这是额外的开销。
我们可以写一个令这个表达式表现更好的 operator+,这种方法在C++03中也是可以的:
// Guess that the first argument is more likely to be an rvalue
Matrix operator+(Matrix x, Matrix const& y)
{
x += y; // x was passed by value, so steal its vector
Matrix temp; // Compiler cannot RVO x, so
swap(x, temp); // make a new Matrix and swap
return temp;
}
Matrix x = a + b + c + d;
一个可以尽可能消除复制的编译器按此实现可以做到近似最优的结果,只创建一个临时对象并将其内容直接转移给 x。但是,以下这种难看的写法可以很容易地让我们的优化失效:
Matrix x = a + (b + (c + d));
实际上,这比我们原来的实现更糟糕:现在右值总是出现在 + 号的右边而被显式地复制。左值却总是出现在左边,但由于它是传值的,所以隐式的复制无法被消除,这样我们得到了六次复制的代价。
不过,有了过右值引用,我们可以通过在原有实现上增加一些重载来进行可靠的优化工作:
// The "usual implementation"
Matrix operator+(Matrix const& x, Matrix const& y)
{ Matrix temp = x; temp += y; return temp; }
// --- Handle rvalues ---
Matrix operator+(Matrix&& temp, const Matrix& y)
{ temp += y; return std::move(temp); }
Matrix operator+(const Matrix& x, Matrix&& temp)
{ temp += x; return std::move(temp); }
Matrix operator+(Matrix&& temp, Matrix&& y)
{ temp += y; return std::move(temp); }
有些类型确实不应该被复制,但是以传值方式传递它们、从函数返回它们、把它们保存在容器中却又非常有意义。一个你可能很熟悉的例子就是 std::auto_ptr
由于这些原因,原标准明确禁止将 auto_ptr 放入标准容器中,而且在C++0x中 auto_ptr 已被淘汰。取而代之的是一个不能复制只能转移的新型智能指针:
template
struct unique_ptr
{
private:
unique_ptr(const unique_ptr& p);
unique_ptr& operator=(const unique_ptr& p);
public:
unique_ptr(unique_ptr&& p)
: ptr_(p.ptr_) { p.ptr_ = 0; }
unique_ptr& operator=(unique_ptr&& p)
{
delete ptr_; ptr_ = p.ptr_;
p.ptr_ = 0;
return *this;
}
private:
T* ptr_;
};
unique_ptr 可以被放在标准容器中,也可以做 auto_ptr 能做的任意事情,除了隐式地从左值转移。如果你想从左值转移,你只要用 std::move 来传递它即可:
int f(std::unique_ptr); // accepts a move-only type by value
unique_ptr x; // has a name so it's an lvalue
int a = f( x ); // error! (requires a copy of x)
int b = f( std::move(x) ); // OK, explicit move
C++0x中的其它只可转移的类型还包括流类型、线程和锁(新的多线程支持),所有的标准容器都可以持有只可转移的类型。
还有很多东西要介绍。在本系列文章的其它主题中,我们将提及异常安全、转移赋值(再次)、完美前转,以及如何在C++03中进行转移。敬请关注!