C++右值引用浅析(一)

2015-01-27 14:01:24 · 作者: · 浏览: 37
直想试着把自己理解和学习到的右值引用相关的技术细节整理并分享出来,希望能够对感兴趣的朋友提供帮助。
?
右值引用是C++11标准中新增的一个特性。右值引用允许程序员可以忽略逻辑上不需要的拷贝;而且还可以用来支持实现完美转发的函数。它们都是实现更高效、更健壮的库。
?
move语义
先不展开具体右值引用定义。先说说move语义。右值引用是用来支持move语义的。move语义是指将一个同类型的对象A中的资源(可能是在堆上分配,也可能是一个文件句柄或者其他系统资源)搬移到另一个同类型的对象B中,解除对象A对该资源的所有权。这样可以减少不必要的临时对象的构造、拷贝以及析构等动作。比如我们经常使用的std::vector,当两个相同的std::vector类型赋值时,一般的步骤如下:
?
内部的赋值构造函数一般是先分配指定大小的内存,
从源std::vector中拷贝到新申请的内存,
之后再把原有的对象实例析构掉,
最后接管新申请的数据。
这就是我们C++11之前使用的拷贝语义,也就是常说的深拷贝。move语义与拷贝语义相对,类似于浅拷贝,但是资源的所有权发生了转移。move语义的实现可以减少拷贝动作,大幅提高程序的性能。
?
而为了实现move语义的构造,就需要对应的语法来支持。原有的拷贝构造函数等不能够满足该需求。最典型的例子就是C++11废弃的std::auto_ptr,其构造函数会产生不明确的拥有权关系,很容易滋生BUG。这也是很多人不喜欢std::auto_ptr的原因。C++11为此增加了相应的构造函数。
?
复制代码
class Foo {
public:
? ? Foo(Foo&& f) {}
? ? Foo& operator=(Foo&& f) {?
? ? ? ? return *this;
? ? }
};
复制代码
这里可以明显看到两个函数中的参数类型是Foo&&。这就是右值引用的基本语法。这样做的目的是通过函数重载实现不同的功能处理。
?
强制move语义
C++11规定即可以在右值上使用move语义,也可以在左值上使用move语义。也就是说,可以把一个左值转为右值引用,然后使用move语义。比如在C++的经典函数swap中:
?
复制代码
template
void swap(T& a, T& b)?
{?
? T tmp(a);
? a = b;?
? b = tmp;?
}?
?
X a, b;
swap(a, b);
复制代码
上面代码中没有右值,但是tmp变量只作用在本函数作用域中,只是用来承担数据的转移动作。C++11制定的上述规则在这里反而可以得到非常好的适用。C++11为了达到这个规则,实现了std::move函数,这个函数的就是把传入的参数转换为一个右值引用并返回。也就是说在C++11下,swap的实现如下:
?
复制代码
template?
void swap(T& a, T& b)?
{?
? T tmp(std::move(a));
? a = std::move(b);?
? b = std::move(tmp);
}?
?
X a, b;
swap(a, b);
复制代码
我们在实际使用中,也可以尽量的多使用std::move。只要求我们自定义的类型实现转移构造函数。
?
右值引用
为了说清楚右值引用什么,就不得不说左值和右值。简单的说左值是一个指向某内存空间的表达式,并且我们可以用&操作符获得该内存空间的地址。右值就是非左值的表达式。可以 阅读这篇《Lvalues and Rvalues》进行深入理解。
?
右值引用非常类似于C++的普通引用,也是一个复合类型。为了方便区分,普通引用就是左值引用。一个左值引用就是在类型后面加&操作符。而右值引用就是在类型后加&&操作符,就像上面的转移构造函数的参数一样。
?
右值引用的行为类似于左值引用,但是右值引用只能绑定临时对象,不能绑定一个左值引用。右值引用的出现还影响了函数重载决议。左值会优先适配左值引用参数的函数,右值会优先适配右值引用参数的函数:
?
复制代码
void foo(X& x); // lvalue reference overload
void foo(X&& x); // rvalue reference overload
?
X x;
X foobar();
?
foo(x); // argument is lvalue: calls foo(X&)
foo(foobar()); // argument is rvalue: calls foo(X&&)
复制代码
理论上,你可以用这种方式重载任何函数,但是绝大多数情况下这样的重载只出现在拷贝构造函数和赋值运算符中,也就是实现move语义。
?
如果你实现了void foo(X&);,但是没有实现void foo(X&&);,那么和以前一样foo的参数只能是左值。如果实现了void foo(X const &);,但是没有实现void foo(X&&);,仍和以前一样,foo的参数既可以是左值也可以是右值。唯一能够区分左值和右值的办法就是实现void foo(X&&);。最后,如果只实现了实现void foo(X&&);,但却没有实现void foo(X&);和void foo(X const &);,那么foo的参数将只能是右值。
?
右值引用是右值吗?
void foo(X&& x)
{
? X anotherX = x;
? // ...
}
在上面这个函数foo内,X的哪个构造函数会被调用?是拷贝构造还是转移构造?按照我们之前说的,这是个右值引用,应该是调用的X(X&&);函数。但是实际上,这里调用的是X(const X&);这里就是让人迷惑的地方:右值引用类型既可以被当做左值也可以被当做右值,判断的标准是该右值引用是否有名字。有名字就是左值,否则就是右值。如果要做到把带有名字的右值引用变为右值,就需要借助std::move函数。
?
void foo(X&& x)
{
? X anotherX = std::move(x);
? // ...
}
在实现自己的转移构造函数时,一些人没有理解这一点,导致在自己的转移构造函数内部的实现中实际是执行了拷贝构造函数。
?
move语义与返回值优化
了解了move语义和强制move以及右值引用的一些概念后,有些朋友在实现一些函数时,会在返回的地方进行强制move。认为这样可以减少一次拷贝。比如:
?
复制代码
X foo()
{
? X x;
? // perhaps do something to x
? return std::move(x); // making it worse!
}
复制代码
实际上这种是不需要的。因为编译器会做返回值优化(Return Value Optimization)。也就是说编译器能够感知到x变量不再需要了,它需要转移到函数外部使用。
?
完美转发
右值引用除了用