C++:模板实参推断及引用折叠
2015-06-14 11:00
253 查看
1.模板实参推断的概念
对于函数模板和类模板,编译器利用调用中的实参函数来确定其模板参数。 从函数实参来确定模板实参的过程,称为模板实参推断。模板实参的推断和常规的函数实参推断的规则不一样,尤其是在类型转换这一方面,两者大不相同。
2. 模板实参推断过程中的类型转换
同非模板函数,在一次调用中传递给函数模板的实参来初始化函数的形参。如果函数形参类型是泛型,采用特殊的初始化规则。1.const转换 : 可以将非const对象的引用或者指针传递给const的引用或指针形参。
2.数组、函数指针转换:如果函数形参不是引用类型(前提!),则可以对数组或者函数类型的实参应用正常的指针转换。(详细解释看例子)
3.其他类型的转换 : 算术转换(
double -> int等)、派生类向基类的转换、用户自定义的转换,都不可以用于模板实参推断
考虑以下两个例子:
template<typename T> void funObj(T x, T y) { cout << typeid(x).name() << endl; cout << typeid(y).name() << endl; } int a[10], b[42]; string s1("a value"); const string s2("another value"); funObj(s1, s2); funObj(a, b);
这两次调用都是合法的。
在
funObj(s1,s2)的调用中,传递了一个string和const string**(s2的值不可改变,是顶层const,编译器会忽略掉),因为T不是引用类型,所以实参被拷贝**,因此原对象是否为const都不影响。
不过考虑下面这组调用:
int i = 0, j = 0; const int *cp = &i; int * p = &i; funObj(cp, p);
对函数
funObj(cp,p)的调用是失败的。
等等,这和我们刚才说的好像有点矛盾,实参被拷贝,因此原对象是否为const不是不影响么?
注意,我们说的原对象是否为const不影响指的是原对象是否为顶层const,对于指针,除了可能为顶层const(指针本身是const),还可能是底层const(指针指向的对象是const),这里cp指向的对象是const,也就是说cp为底层const,模板实参推断的时候不会忽略底层const,实际上这里调用
funObj(const int *,int *),编译器拒绝这样做。
在
fun(a,b)的调用中,传递了一个int [10] 和一个int[42],因此T不是引用类型,且a,b的数组类型相同,所以T被隐式转换为
int*,对应规则2.
如果另外添加一个变量
double c[10],然后调用
funObj(a,c)是无法通过编译的,虽然double和int是可以进行隐式转换的,不过遵循规则3,因此编译器不予通过。
第二个例子:
template<typename T> void funRef(const T& x, const T& y) { cout << typeid(x).name() << endl; cout << typeid(y).name() << endl; } funRef(s1, s2); funRef(a, b);
T是const引用,在调用
funRef(s1,s2)中,相当于
funRef(const string &,const string &),这是因为将s1转换为const是允许的。
考虑如下函数:
template<typename T> void funRef_noConst(T& x, T& y) { cout << typeid(x).name() << endl; cout << typeid(y).name() << endl; } funRef_noConst(s1, s2);
因为虽然不能将一个const引用传递给非const引用,但是模板参数推断时会忽略掉顶层const,所以
funRef_noConst(s1,s2)相当于调用
funRef_noConst(const string&,const string&)相当于优先匹配const,将其他非const的转换为const引用。
如果想更改s2,应该考虑下述方法:
template<typename T1,typename T2> void funRef_noConst(T1& x, T2& y) { cout << typeid(x).name() << endl; cout << typeid(y).name() << endl; } funRef_noConst(s1, s2);
此时相当于调用`funRef_noConst(string & ,const string &)
在调用
funRef(a,b)中,因为T是引用类型,根据规则2,数组类型不匹配,当然,如果换成上述的
funRef_noConst(T1 & x,T2 & y)就可以通过编译。相当于调用
funRef_noConst(int (&x)[10],int (&y) [42])。
3.模板显式实参
某些情况下编译器可能无法自动推断模板实参的类型某些情况下我们希望自己控制实例化过程
为了满足这两种需求,C++允许用户提供显式模板实参。
考虑这种情况:
template<typename T1,typename T2,typename T3> T1 sun(T2 x, T3 y) { return x + y; }
因为T1没有出现在函数的参数列表中,编译器不能自动推断T1的类型,所以调用时我们需要为T1提供一个显式模板参数。例如
sum<int>(x,y)。
显式模板参数的指定规则是从左往右依次匹配,因此
int首先替换T1。
但是如果对
sum换一种声明方式
template<typename T1,typename T2,typename T3> T3 sun(T2 x, T1 y) { return x + y; }
这是非常不好的策略,每次调用都必须
sum<int,int,long>(x,y)这样指定所有参数,很不方便。
不过,也是有好处的~正常的类型转换应用于实参指定的模板
template<typename T> T sum(T x,T y) { return x + y; } int x; double y; sum<int>(x,y)
对
sum指定了所有的实参类型(这里sum只有一个,如果有多个T都需要指明),那么正常转换开启,double和int尽管类型不同,但是可以发生隐式转换,所以编译通过。
4.模板实参推断和引用
提到引用参数,一般来讲c++新标准中有三种。左值引用
const左值引用
右值引用
这三种对应不同的推断规则,一一介绍。
1.模板参数为左值引用:
考虑模板:
template<typename T> void funRef(T & arg) { cout << typeid(arg).name() << endl; }
这里
arg是一个普通的左值引用,这意味着我们只能传递一个左值实参。
funRef(i);//i是一个int,模板参数类型T是int funRef(ci);//ci是一个const int,顶层const会被忽略,T是const int funRef(5);//wrong,5是字面值常量,右值
2.模板参数为const引用
如果一个函数参数类型为
const T &,正常绑定规则告诉我们可以传递给它任何类型实参 ————对象、临时对象、字面值常量
考虑函数:
template<typename T> void funRef(const T & arg) { cout << typeid(arg).name() << endl; } funRef(i);//i是一个int,模板参数类型T是int funRef(ci);//ci是一个const int,但是T是一个int funRef(5);//T是int
3.参数类型为右值引用:
如果模板参数为右值引用,这意味着我们可以一个右值。
template<typename T> void funRef(T && arg) { cout << typeid(arg).name() << endl; } funRef(5);//5是一个右值,模板参数T为int
直到这里都是和我们直觉统一的,但是神奇的是
funRef(i);//i是int,左值 funRef(ci);//i是const int,左值
这两种调用都成功了!
这里就是我们要说的,我感觉设计有点奇葩的地方————引用折叠规则。
5.引用折叠
一般来讲,我们不能定义一个引用的引用,但是通过类型别名或者模板参数可以间接定义。引用折叠规则适用于这种情况。当我们间接创建一个引用的引用,这些引用形成了“折叠”。
对于一个给定的类型X,下面是折叠规则:
X& . & . X& && . X && &都被折叠为
X&
X&& &&折叠为
X &&
除了引用折叠规则之外,c++标准还规定了一个“右值转换”的规则。
右值转换的含义是:
形容
funRef(T && arg)的这种参数为右值引用的模板,当实参为一个左值时,调用仍然成功,此时编译器推断模板参数(也就是T)为左值的引用。例如调用
funRef(i),那么
T是int&而非
int,展开可知
funRef(int & && arg)再采用上述的引用折叠规则,可知最后
arg是
int&
funRef(i);//实参是左值,int,T是int& funRef(ci);//实参是左值,const int,T是const int &
以上这两条规则意味着,我们可以将任意类型的实参传递给
T&&类型的函数参数。
我们可以看一下C++ Primer 第五版 练习16.45
给出模板:
void g(T && value) { vector<T> v; } int x; g(42);//解释会发生什么 g(x);//解释会发生什么
在调用
g(42)这条语句,42是一个右值
T会被推断为
int,相应的
value被推断为
int&&,调用
vector<T>即调用
vector<int>
在调用
g(x)这条语句,因为x是一个左值,根据“右值转换”规则,
T被推断为int&,展开g得到
g(int & &&)最后引用折叠规则得到
g(int & value,对value的更改会影响到x。由于
T是int&,当调用
vector<T>时,就调用
vector<int&>,又引用必须被初始化,所以这里就会出错,编译不过。
相关文章推荐
- 使用Google CPU Profiler对C/C++多线程程序做性能剖析
- C++ 虚函数 剖析
- Effective C++条款04解读:确定对象被使用之前已先被初始化
- C++ Primer Plus学习笔记二(第三章)
- C++多线程与临界资源实例
- C++关于变量名的解析
- C语言编译过程简介
- C语言中的指针-记录个人理解
- STL控件的使用
- c++ 头文件<cstring>(或者string.h)中的常见函数的实现!
- chapter12test6
- 一起talk C栗子吧(第十五回:C语言实例--双向链表)
- c++ 在控制台里用字符拼成图片
- C++11特性(01)auto关键字
- C++ I/O
- C++对象模型之RTTI的实现原理
- c/c++与java------之JNI学习(一)
- C/C++中问号冒号表达式的陷阱
- poj1062昂贵的聘礼有等级限制的最短路径
- 《C++Primer》读书笔记--异常处理