简介
右值引用仅会绑定到右值,主要用于识别出可移对象。
可以绑定到左值、右值、const、非const、volatile、非volatile等等一切对象的引用,称为万能引用。
右值引用和万能引用都使用形如“T&&”的符号表示。
下面做个小测试,试着区分一下下面的引用属于万能引用还是右值引用:
// 1.
void f(Foo&& p);
// 2.
Foo&& var1 = Foo();
// 3.
auto&& var2 = var1;
// 4.
template<typename T>
void f(std::vector<T>&& p);
// 5.
template<typename T>
void f(T&& p);
识别万能引用
先说上面测试的结果:3和5是万能引用,其他都是右值引用。
可见,只有涉及类型推导的引用,才会是万能引用。
另外一个条件,就是它的形式必须是“T&&”,上述第4个语句因为不符合该条件,所以只是个右值引用。
基于这个原因,即使是一个const饰词,一个看上去是万能引用的场合可能也只是个右值引用。
对万能引用实施std::forward
首先需要注意一点,对右值引用使用std::move没有任何问题。但对万能引用使用std::move会导致问题。
因为对于万能引用,如果采用右值来初始化,得到的是一个右值引用,如果采用左值来初始化万能引用,那么得到的是一个左值引用。
所以如果对万能引用使用std::move,不管是左值初始化还是右值初始化,那么得到的只能是右值,这可能不是你希望看到的结果。
考虑下面的代码:
class Foo {}
public:
template<typename T>
void SetName(T&& new_name) {
name_ = std::forward<T>(new_name);
}
//...
private:
string name_;
;
说明如下:
- 在模板内,new_name 是万能引用,赋值时采用万能引用,这样不管是左值调用还是右值调用,都将得到合理的结果
- 如果把赋值语句修改为:name_ = std::move(new_name); 将会导致以下问题:
- 当以一个局部变量调用SetNmae时,调用者会合理地假设这是一个对参数的只读操作。但实际上参数已经是一个不确定的值,改变了意图
不要对RVO施加move转换
如果代码:
Foo MakeFoo()
{
Foo f;
//...
return f;
}
有人可能会觉得,既然使用std::move有机会把复制构造转换为移动构造,那么对返回值使用std::move也可以达到同样的优化目的了。
“优化”后的代码如下:
Foo MakeFoo()
{
Foo f;
//...
return std::move(f);
}
先结论,这样做是不正确的,它可能并不会达到优化的目的,甚至很可能比上一个代码更糟糕。
原因如下:
- C++标准已经规定了返回值优化(return value optimization, RVO),通过直接在为函数返回值分配的内存上创建局部变量f来避免复制
- 上述代码符合RVO条件,编译器会为它作出优化,所以它实际上不会复制对象,效率极高
- 使用move后,它移动f的内容到MakeFoo的返回值空间,而这比RVO多出一些操作,必然更慢
- 但有一点需要注意,执行RVO需要同时满足以下条件,缺一不可:
- 局部对象类型和函数返回值类型相同
- 返回的就是局部对象本身
- 即使未满足上述条件,c++标准仍然规定,编译器需要把返回对象作为右值处理
综上,对返回值执行std::move就是多此一举、画蛇添足。
小结
右值引用和万能引用为我们实现期望的功能提供了强大的助力,使用得当可得到如虎添翼的效果。
但也要注意里面涉及的细节,稍不小心,也可能埋下以后加班调试的坑。
参考资料
《Effective Modern C++》