当你准备迫切地想要了解本文的内容时,说明你对c++的了解已经比较深入了!本文将试图介绍c++11之后的模板类型推导机制是怎样的(加入右值引用概念),诚然,模板类型推导规则是了解GP(泛型编程,Generic Programming)的基础和前提,但只会这个还远远不够,泛型编程对我来说还是一个触不可及的范式;哪怕你不准备学习使用GP,模板类型推导规则仍然是一个合格的c++工程师必备的知识,模板在现代c++开发中还是很常用的!
c++的函数/类模板为我们提供了这样一种语义:可以针对多种不定的类型作为参数设计类或者函数,而这些类型在设计类或函数时时暂时不能确定,用 模板参数 来暂为代表:
template<class T>//只有在模板参数声明中,class=typename
class A;
template<typename T>
void fun(T arg);
就像这样,我们可以暂且以一个模板参数T,代替以后将要传入的类型,编译器则负责在调用时推导出T的类型,然后将模板具现化:
template<typename T>
void fun(T arg);
int main(){
int a=2;
fun(a);//调用时,编译器会生成这样的声明源码:void fun(int arg)
}
可见,模板体系可以正确运作的前提就是必须正确按照用户的调用,推导出模板参数的类型,然后生成具现化版本就容易了。看到这你会问,这。。有啥可讲的呢?模板的推导应该都是编译器级别的东西啊,我们放心交给编译器就好了嘛,学这个有啥用呢?如果你一开始是这样想的,那么可以看看下面这些例子并说出编译器会将模板参数推导为什么?
template<typename T>
void fun_r(T& arg);//模板参数为左值引用(指针也可)
template<typename T>
void fun_rr(T&& arg);//模板参数为万能引用
template<typename T>
void fun(T arg);//模板参数为类型
int main(){
int a=0;
//下面这些调用,编译器会将模板推导成什么类型?
fun_r(a);//答案是:void fun_r(int& arg)
fun_rr(a);//答案是:void fun_rr(int& arg)
fun(a);//答案是:void fun(int arg)
}
上面的结果和你预期的一样吗?如果你是初学者的话,很可能会对上面的结论感到怀疑和困惑。因此可以发现,我们学习模板类型推导的目的,主要是为了研究**“当模板函数的形参具有引用属性时,传入具有不同引用属性的实参,编译器会如何推导出其形参类型?”**上面的例子,把这个问题分成了3个子问题,我们只需分别搞清楚:当模板函数形参类型为模板参数的左值引用(指针)、万能引用、值类型 时,模板类型推导的行为。除此以外,还有2种特殊情况需要说明:传递数组实参、传递函数实参。
下文所述的表达式含义如下代码:
template<typename T>
void fun(Type arg);//模板参数为类型
fun(expr);
T为模板参数、Type为上述3种T的引用属性类型、arg为模板函数形参、expr为调用时传入的实参。
即,模板函数为如下形式:
template<typename T>
void fun(T& arg);
此时的规则如下:
举个栗子:
//调用处:
int a=0;
int& b=0;
const int c=0;
const int& d=0;
fun(a);//T->int, arg->int&
fun(b);//T->int,arg->int&
fun(c);//T->const int,arg->const int&
fun(d);//T->const int,arg->const int&
fun(2);//不接受右值!
可见,传入参数的饰词也会被保留下来算作T的推导类型,毕竟T的推导只是去掉引用属性嘛,别的都保留下来。类似,如果Type类型中含有其他饰词则也会被保留下来作为arg的形参推导类型。
你可能不是很清楚万能引用是个啥?可以这样理解:万能引用的写法和右值引用一样,但如果出现在模板函数形参类型中,这就不叫右值引用,而叫做万能引用。为什么叫“万能引用”呢?因为万能引用比较“全能”,可以推导出你实际传入参数的左值or右值属性类型。
规则如下:
template<typename T>
void fun(T&& arg);
//调用处:
int a=0;
fun(a);//T->int&,arg->int& && -> 引用折叠 -> int&
fun(2);//T->int,arg->int&&
template<typename T>
void fun1(const T&& arg);//不算万能引用!(按左值引用的规则来推导)
template<typename T>
void fun2(std::vector<T>&& arg);//不算万能引用!(按左值引用的规则来推导)
template<typename T>
class A{
void fun3(T&& arg);//不算万能引用!(按左值引用的规则来推导)
};
可见以下几点结论:
T&&
或auto&&
(接下来的文章会讲到auto)的形式才能获得万能引用的好处,任何“小聪明”都是不行的。停等会儿!上面的代码中:a,b作为实参时,推导出来的arg形参是个什么玩意啊啊啊啊?int& &&
??? 要解释这是个啥,就要搬出引用折叠
的概念了!
引用折叠:
引用折叠规则也是c++11引用右值引用后,不得不引入的一种规则,使用场景几乎仅在万能引用的类型推导上,引用折叠的具体规则如下:
注意,这些引用之间是有空格的,意味着:“左值引用的右值引用”…诸如此类。通过以上4条规则,可以概括成1条通用原则:**发生引用折叠时,只要带有一个左值引用,结果就是左值引用;只有当折叠的所有引用都是右值引用,结果才是右值引用。**这句话有点绕,但结合上面的4条,应该可以很快理解。
补上引用折叠的知识,就可以解释为什么上面代码中奇怪的表述:int& &&
等价于 int&
了。
即,模板函数为如下形式:
template<typename T>
void fun(T arg);
这种情况下其实不要把它想复杂了,和引用无关的话反而简单,相当于形参是实参的拷贝(和实参没啥关系了)。规则如下:
举个栗子:
template<typename T>
void fun(T arg);
//调用处:
int a=0;
int& b=0;
const int& c=0;
const int* const d=&a;
fun(a);//T->int,arg->int
fun(b);//T->int,arg->int
fun(c);//T->int,arg->int
fun(d);//T->const int*,arg->const int*
fun(2);//T->int,arg->int
template<typename T>
void fun1(const T arg);
//调用处:
int a=0;
fun(a);//T->const int,arg->const int
//诸如此类...
c语言使用者都知道,数组和指针有着紧密的联系,但应该意识到,即便是c语言,也会把指针和数组看成是两个不同的类型,只不过数组可以隐式转换为指针而已,这称为数组退化,比如下面的经典情况:
void func(int*);
//调用处
int arr[10];
func(arr);
这样数组就退化成了指针,但在c语言中,你也可以让他不退化地传入函数啊:
void func(int arr[10]);
//调用处
int arr[10];
func(arr);
这样就没有发生退化,一个完整的数组被传入函数了,而不是只有首地址的指针。当然了,在现代c++中深究这个问题没有太大意义(std::array),知道有这么回事,能看懂就行了。
我们要讨论的问题是,当数组传入模板函数时,会发生这种退化吗?直接看结论:
值类型的形参,传入数组退化为指针;而引用类型的形参,传入数组不退化。
template<typename T>
void fun(T arg);
template<typename T>
void fun1(T& arg);
//调用处
int arr[10];
fun(arr);//数组退化,T->int*,arg->int*
fun1(arr);//不退化,T->int[10],arg->int(&)[10](数组引用)
和数组类似,函数即便在c中也是一种类型的,而却可以很容易地退化成函数指针。
这样,道理就与数组几乎一样了,当一个函数传入模板函数时,有着和数组类似的结论:
值类型的形参,传入函数退化为函数指针;而引用类型的形参,传入函数不退化。
template<typename T>
void fun(T arg);
template<typename T>
void fun1(T& arg);
//调用处
void cfunc(int);
fun(cfunc);//函数类型退化,T->void(*)(int),arg->void(*)(int)(函数指针)
fun1(cfunc);//不退化,T->void(int),arg->void(&)(int)(函数类型引用)