20.2 C++高级话题与新标准-万能引用

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> &&param){...}//右值引用

    题目3这里为什么是右值引用呢?因为T跟“&&”不挨着(T跟“&&”必须挨着,这个形式就得是这样T&&),所以是右值引用,可以进行如下测试:

template<typename T>
void func(std::vector<T> &&param){}

在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表示右值也可以
}

    如果搞不清楚形参类型是否是一个万能引用,则分别传递进去一个左值和一个右值作为实参来调用,就可以验证

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值