2.万能引用
2.1 类型区别基本概念
这部分比较重要,又容易被忽略,请读者看仔细。
看看下面这行代码:
void func(const int& abc) {}
现在如果问:“abc是什么类型”?读者可以脱口而出:“constint &”类型,这没错,因为在那写着呢!
现在把问题深入一点。把这个func函数改造成一个函数模板:
template <typename T>
void func(const T& abc) { }
func(10);
现在问题来了:请问T是什么类型?abc是什么类型?
笔者抛出这个问题的时候,相信有些读者会突然意识到一些问题:T是有类型的啊?abc也是有类型的啊?当然了,T是一个类型模板参数,当然有类型,abc是一个变量,当然也有类型。而且T的类型和abc的类型往往不同。例如这里,abc是用const来限定的。所以对于func(10);这行代码,真实的结果是:
· T的类型是int。
· abc的类型是constint&。
通过观察,觉得T的类型之所以是int,是因为进行函数调用的时候给的参数是10。这样考虑是对的,确实因为传递进去的是10,导致T推断出来的是int类型。但是这句话说的不全面,T的类型到底是什么不仅仅取决于调用这个函数模板时给进来的参数10,还取决于abc的类型(也就是还取决于constT&)。那么,下面就要讲一讲abc的类型为万能引用时是如何对T的类型产生影响的(不过这个话题可能一节讲不完,要分多节讲,慢慢来)。其他情况对T类型产生的影响,以后再讲。
2.2 universal reference基本认识
universal reference(后来被称为forwarding reference:转发引用)翻译成中文有好几种翻译方法,笔者取两种最常见的翻译,一种叫“万能引用”,一种叫“未定义引用”,都是一个意思,后续就称为“万能引用”了。
万能引用这个概念是讲解后面的一些C++11新标准知识的基础概念,所以在这里率先把这个概念给读者阐述清楚。首先记住一个结论:万能引用是一种类型。就跟int是一种类型一个道理,再次强调,万能引用是一种类型。
在14.12节讲解了右值、右值引用的概念。右值引用用两个“&&”符号表示,读者都知道了。右值引用主要是绑定到右值上,例如:
int &&rv = 1000;
现在举一个例子,在MyProject.cpp的前面位置增加一个函数定义:
void myfunc(int&& tmprv) //参数tmprv是个右值引用类型
{
cout << tmprv << endl;
return;
}
在main主函数中,加入如下代码:
{
myfunc(10); //正确,右值做实参
int i = 100;
myfunc(i); //错,右值引用不能接(绑)左值
}
从以上代码中得到了一个结论:右值引用肯定不能接左值(形参为右值引用类型,给的实参不能是左值)。编译的时候编译器给出的报错信息是:无法将参数1从“int”转换为“int &&”。
现在将myfunc函数改造成函数模板,请看:
template <typename T>
void myfunc(const T&& tmprv) //有const修饰,因此万能引用资格被剥夺,因为是&&所以只能是个右值引用
{
cout << tmprv << endl;
return;
}
现在观察和思考一下,如果上面的T是一个int型,那么这个函数模板myfunc用int类型实例化后得到的函数似乎和普通的myfunc函数感觉应该是一样的。带着这种感觉编译程序,居然发现main主函数中的代码行“myfunc(i);”不再报错。显然这个感觉是不对的。为什么写成函数模板之后,“myfunc(i);”代码行的调用就不再报错了?
现在看到的事实有两条:
· 第一条就是这里的函数模板中的tmprv参数能接收左值(作为实参),也能接收右值。
· 另一条看到的事实是tmprv的类型是T&&(这两个地址符是属于tmprv的),编译都没报错。
本来以为myfunc函数模板中这个T会被推断成int型,但从现在这种编译器并没有报错的情况来看,事情并不是预料的这样。T本身应该没有被推断成int型,因为int型已经被证明“myfunc(i);”调用时编译器会报错。
现在再观察一个事实,发现只有在函数模板中发生了类型模板参数推断(简称函数模板类型推断)的时候才出现这种tmprv参数既能接收左值,又能接收右值,编译器都不报错。没错,这就引出了本节要讲解的新知识:万能引用。
万能引用(又名未定义引用,英文名universal reference)离不开上面提到的两种语境,这两种语境必须同时存在:
· 必须是函数模板。
· 必须是发生了模板类型推断并且函数模板形参长这样:T&&。
以后还会讲到auto类型推断也存在万能引用的概念,不过现在先记住这两种语境。
所以万能引用就长这样:T&&。它也用两个地址符号“&&”表示,所以万能引用长得跟右值引用一模一样。但是解释起来却不一样(注意语境,只有在语境满足的条件下,才能把“&&”往万能引用而不是往右值引用解释):
(1)右值引用作为函数形参时,实参必须传递右值进去,不然编译器报错,上面已经看到了。
(2)而万能引用作为函数形参时,实参可以传递左值进去,也可以传递右值进去。所以,万能引用也被人叫作未定义引用。如果传递左值进去,那么这个万能引用就是一个左值引用;如果传递右值进去,那么这个万能引用就是一个右值引用。从这个角度来讲,万能引用更厉害:是一种中性的引用,可以摇身一变,变成左值引用,也可以摇身一变,变成右值引用。必须再次提醒读者,T&&才是万能引用,千万不要理解成T是万能引用,其实,tmprv的类型是T&&,所以tmprv的类型才是万能引用类型(或者理解成tmprv才是万能引用),这意味着如果传递一个整型左值进去,tmprv的类型最终就应该被推断成int&类型;如果传递一个整型右值进去,tmprv最终就应该被推断成int&&类型。
所以,根据上面提到的万能引用存在的两种语境,可以确认myfunc这个函数模板里的T&&并不是右值引用(只能绑定到右值),而是一个万能引用。
现在掌握了万能引用T&&,笔者要求读者务必记住,万能引用长得跟右值引用虽然像,但是万能引用存在的场景要求T是类型模板参数,后面必须跟两个“&&”,也就是“T&&”,不满足这个条件的,都不是万能引用。
总之,通过讲解的这些内容,得到了一个结论:T&&是一个万能引用类型。
下面给出一些面试中的判断题,请尝试分析。判断如下的参数类型是右值引用还是万能引用:
题目1:
void func(int && param){...}//右值引用 因为func不是函数模板而是一个普通函数
题目2:
template<typename T>
void func(T && temvalue){...}//是万能引用
题目3:
template<typename T>
void func(std::vector<T> &¶m){...}//右值引用
题目3这里为什么是右值引用呢?因为T跟“&&”不挨着(T跟“&&”必须挨着,这个形式就得是这样T&&),所以是右值引用,可以进行如下测试:
template<typename T>
void func(std::vector<T> &¶m){}
在main主函数,加入如下代码
vector<int> aa = {1};
func(std::move(aa)); //不用std::move不行 也就是说 用左值当参数当参数传递是不行的
所以以后如果读者看到下面的情况才是万能引用:
(1)一个是函数模板中用作函数参数的类型推断(参数中要涉及类型推断),形如T&&,另一个看下边。
(2)auto&&tmpvalue=…也是一个万能引用,这个到时候再详细介绍。
其他的看到的“&&”的情况都是右值引用。
下面继续研究一下上述的myfunc代码段。完整的myfunc代码现在是如下的样子:
template <typename T>
void myfunc(T&& tmprv) //万能引用,注意 &&是属于tmprv类型的一部分,不是T类型的一部分(&&和T类型没有关系)
{
tmprv = 12; //不管tmprv的类型是左值引用还是右值引用,都可以给tmprv赋值
cout << tmprv << endl;
return;
}
{
int i = 100;
myfunc(i); //左值被传递,因此tmprv是左值引用,也就是类型为int &。执行完毕后,i值变成12
i = 200;
myfunc(std::move(i)); //右值被传递,因此tmprv是右值引用,也就是类型为int &&。执行完毕后,i值变成12
cout << "断点设置在这里" << endl;
}
2.3 万能引用资格的剥夺与辨认
(1)剥夺
const修饰词会剥夺一个引用成为万能引用的资格,被打回原形成右值引用。
下面改造一下myfunc,注意看代码:
template <typename T>
void myfunc(const T&& tmprv) //有const修饰,因此万能引用资格被剥夺,因为是&&所以只能是个右值引用
{
cout << tmprv << endl;
return;
}
此时,在main主函数中,代码如下,注意看代码中的注释部分:
int i = 100;
myfunc(i);//不可以 只能传递右值进去 必须是myfunc(std::move(i));
所以,请注意T&&前后左右都不要加什么修饰符,不然很可能就不是万能引用而直接退化为右值引用了。
(2)辨认
template <typename T>
class mytestc
{
public:
void testfunc(T&& x) {}; //这个不是万能引用,这个是右值引用
template <typename T2>
void testfunc2(T2&& x) {} //T2类型是独立的,和T没任何关系,而且x是函数模板形参,类型是推导来的,所以这是个万能 引用
};
int main()
{
mytestc<int> mc;
int i = 100;
mc.testfunc(i); //错,左值不能绑定到右值引用上,必须修改为:mc.testfunc(std::move(i));
}
问题来了,为什么这里testfunc后面的T&&不是一个万能引用,而是一个右值引用?因为testfunc成员函数本身没有涉及类型推断,testfunc成员函数是类模板mytestc的一部分。首先得用如下语句来实例化这个类模板成一个具体的类:
mytestc<int> mc;
实例化完这个类之后,mytestc这个类存在了,那么testfunc这个成员函数才真正地存在了。所以testfunc成员函数存在的时候就已经成如下这个样子了:
void testfunc(int && x){};
所以笔者说,testfunc成员函数本身没有涉及类型推断,所以这个形参x是右值引用,不是万能引用。
修改一下mytestc类模板,向其中增加一个public修饰的成员函数模板。现在完整的mytestc类模板定义如下:
template <typename T>
class mytestc
{
public:
void testfunc(T&& x) {}; //这个不是万能引用,这个是右值引用
template <typename T2>
void testfunc2(T2&& x) {} //T2类型是独立的,和T没任何关系,而且x是函数模板形参,类型是推导来的,所以这是个万能 引用
};
int main()
{
mytestc<int> myoc;
int i = 10;
myoc.testfunc2(i); //左值可以,给个数字3表示右值也可以
}
如果搞不清楚形参类型是否是一个万能引用,则分别传递进去一个左值和一个右值作为实参来调用,就可以验证。