C++11 如何区分右值引用与万能引用
之所以说要区分右值引用与万能引用,是因为它们本身存在类似的地方,长得很像(至少在形式上是这样子的);这样的类似之处容易让人产生混淆,沟通时引起歧义,所以要好好的区分它们,到底什么是右值引用(rvalue reference),什么是万能引用(universal reference)。
先看一段代码,理解一下右值引用与万能引用
1. void f(Widget&& param); //右值引用
2. Widget&& var1 = Widget(); //右值引用
3. auto&& var2 = var1; // 万能引用
template<typename T>
4. void f(std::vector<T>&& param); //右值引用
template<typename T>
5. void f(T&& param); //万能引用
右值引用:必须形如 "T&&"的表示,而且最重要的一点是它具有明确的数据类型,不需要进行类型推导。
万能引用:形如 "T&&" 的万能引用,其有两层含义:一种当然是作为右值引用,可以绑定到一个右值;另一种含义是它还可以绑定到一个左值成为左值引用;它甚至可以绑定到一个const对象或者非const对象,volatile对象或者非volatile对象,一个既带const又带volatile的对象。这么厉害的一个引用,几乎可以绑定到一切对象,所以成就了它的大名,万能引用。
万能引用出现的场景
万能引用一般会出现在两种场景中,正是上面代码片段中的示例3(auto声明)和示例5(模板函数形参类型)。其中最常见的场景就是作为模板函数形参类型,其次是作为auto声明一个引用。
以上两种场景中都有一个共同之处:必须进行类型推导,这也只是成为万能引用的必要条件;如果看到了一个引用形如 "T&&",却没有发生类型推导,则它肯定是个右值引用,如上代码片段示例1与示例2;
万能引用需要区分初始化物是左值还是右值来决定最终推导的类型是左值引用还是右值引用,如下代码片段:
template<typename T>
void f(T&& param);
Widget w;
f(w); // w 是个左值,param的类型为左值引用 Widget&
f(std::move(w)); //std::move(w) 是个右值, param的类型为右值引用 Widget&&
成为万能引用的充分条件:必须形如 "T&&",多一个修饰词都不行,这个形式被限定的很死,声明形式必须正确无误。再回顾上面示例4的代码:
template<typename T>
void f(std::vector<T>&& param); // param 是个右值引用
在函数 f 被调用时,T 的类型将被推导,但是param参数的类型形式并不是 "T&&",而是 "std::vector<T>&&",这个条件不成立同样不能成为万能引用。如果觉得有疑问的话,还有一种验证方式,就是调用 f 的时候给它传递一个左值实参,如果不报错,说明它是个万能引用;反之,它是右值引用,因为右值引用不可能会绑定到一个左值上;编译器会很乐意帮您检测出这种错误。
即使是一个 const 修饰词,也足以剥夺一个引用成为万能引用的资格;如下代码:
template<typename T>
void f(const T&& param); // param 是个右值引用
如果在一个模板内看到了函数形参为 "T&&",也不一定就是万能引用,因为可能不会发生类型推导呢;例如标准库里面 vector 的成员函数 push_back 就是这样的情况:
template<class T, class Allocator = allocator<T>> //来自C++标准
class vector {
public:
void push_back(T&& x);
...
}
push_back 函数(该函数有另外一个重载版本,是const类型的)的形参具备万能引用的正确形式,但是在调用 push_back 函数时没有发生类型推导;因为在调用函数之前,肯定已经有了特定的 vector 实例,T 的类型已经确定,所以在调用 push_back 的时候就不会发生类型推导了。
std::vector<int> vv;
//通过 vv 实例化 vector 的模板如下
class vector<int, allocator<int>> {
public:
void push_back(int&& x); // 右值引用
}
作为对比与push_back有类型功能的另一个成员函数emplace_back的形参类型却是个万能引用,因为它真正发生了类型推导,看下面代码片段:
template<class T, class Allocator = allocator<T>>
class vector{
public:
template<class...Args>
void emplace_back(Args&&...args);
};
emplace_back其实是个模板函数,它的参数类型Args是个可变参数包,可以理解为将很多个形参的类型打成一个包整体传进来,Args是独立于T的,每次调用emplace_back函数时,Args都将被推导,而且形式又符合"T&&"的条件,所以Args是个万能引用。
需要注意的一点是:并不是所有万能引用的名称都一定是"T",只要符合必须发生类型推导而且必须形如"T&&"的这两个条件,就是万能引用,叫什么名称都无所谓。
template<typename MyType>
void someFunction(MyType&& param); //param就是个万能引用
之前也讨论了万能引用出现的第二个场景,auto&& 声明一个变量;
auto&& param = var1; //param是个万能引用
像这种场景出现的几乎都是万能引用,因为它们都符合既会发生类型推导而且形如"T&&"的条件;这种场景在C++11里面出现的比较少,但在C++14里面露面的机会就有很多,比如可以作为lambda表达式的形参类型,如下代码片段:
auto TimerFunc = [](auto&& func, auto&&... param){
//执行可调用对象func
std::forward<decltype(func)>(func)(std::forward<decltype(param)>(param)...);
};
以上代码中,func是一个可以绑定到任何可调用对象的万能引用,可以是左值或者右值;param是0个或多个万能引用(一个万能引用参数包),可以绑定到任何数量任意类型的对象上。
正是由于万能引用的出现,才使得很多抽象而又难以表达的东西变得如此简单,也在很大程度上方便了接口的设计以及代码的编写。以上讨论了万能引用与右值引用的区别,希望能帮到您。