您的位置:首页 > 编程语言 > C语言/C++

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&>
,又引用必须被初始化,所以这里就会出错,编译不过。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: