模板类型推导
文章目录
正文部分
对于函数模板:
大多的函数模板均为如下的形式:
ParamTypetemplate<typename T>
void f(ParamType param);
而一次调用则很简单:
f(expr);
在编译期间会通过expr 推导两个类型:一个是T的类型另一个是ParamType的类型,这两个类型一般不相同,因为ParamType常常会包含一些限定符:
如const或引用符号等:
template<typename T>
void f(const T& param);
int x;
f(x);
在本例中,T会被推导为int, 而ParamType则会被推导为const int&
如果所有的情况都如同上述的情况,是否T的类型就是expr的类型呢?
但实际上这一点并不总是成立,T的类型推导结果不仅仅依赖于expr的类型,还依赖于ParamType的形式,具体需要分为三种情况:
- ParamType 具有指针或者引用的类型,但不是一个万能引用
- ParamType 是一个万能引用
- ParamType 既不是指针也不是引用
情况1:ParamType 是指针或者引用的类型,但不是一个万能引用
这种情况是最为简单的,在编译期间的类型推导中:
- 如果expr具有引用类型, 先将引用部分忽略。
- 之后对expr的类型和ParamType的类型执行模式匹配,决定T的类型
例如:
template<typename T>
void f(T& param);
int x = 777;
const int cx = x;
const int& rx = x;
f(x); // T的类型是int, param的类型是int&
f(cx); // T的类型是const int, param的类型是const int&
f(rx); // T的类型是const int, param的类型是const int&
上面的例子3中即使rx是引用类型,而T也没有被推导成一个引用,原因就在于上面说的,expr的引用类型会先被忽略
如果吧函数模板改成如下的形式, 其他地方不变:
void f(const T& param);
则:
f(x); // T的类型是 int, param的类型是const int&
f(cx); // T的类型是 int, param的类型是const int&
f(rx); // T的类型是 int, param的类型是const int&
其余不变。
如果将引用换成指针, 则推导规则也不会发生变化,只会需要将上面的所有引用符号&换成指针符号*即可:
情况2 :ParamType是一个万能引用
例如:
template<typename T>
void f(T&& param);
int x = 777;
const int cx = x;
const int& rx = x;
f(x); // x 是一个左值,所以T的类型是int&, param的类型是int&
f(cx); // cx是一个左值 T的类型是const int&, param的类型是const int&
f(rx); // rx是一个左值 T的类型是const int, param的类型是const int&
f(27); // 27是一个右值,所以T的类型是 int, param的类型是 int&&
具体的万能引用与右值引用如何区分,在后面的条款24中会有详细解释,这里就先看一下。留个印象
ParamType 既不是指针也不是引用
此时即是所谓的按值传递:
template<typename T>
void f(T param);
此时, 无论传入的是什么, param都会是它的一个副本,即一个全新的对象
而如何从expr推导出T的规则如下:
- 如情况1, expr如果具有引用类型,则忽略引用部分
- 忽略expr的引用之后,如果expr是个const 对象,也忽略, 如果是个volatile对象,也忽略(不常用)
所以
int x = 777;
const int cx = x;
const int& rx = x;
f(x); // T 和param均是int
f(cx); // T 和param均是int
f(rx); // T 和param均是int
注意,如同规则所说,即使cx和rx都是const ,但param仍然不具有const类型。
这是合理的,因为param是一个完全独立于cx和rx的对象,即是cx和rx的副本,而cx和rx虽然不可修改,但并不能说明param是否可以修改
还有一点,const 仅仅会在按值传递的部分被忽略,如果形参自己是一个const的指针或引用,expr的常量星会在类型推导的过程中保留。
如果是如下的情况:
template<typename T>
void f(T param); // param 仍然是按值传递
const char* const ptr = "A str" // ptr 是一个指向const对象的const指针
f(ptr); // 传递类型为 const char *const 的实参
这个ptr指针自己会被按值传递,param的类型会被推导为 const char *,即一个可修改的,指向const字符串的指针。
数组实参
数组类型其实与指针类型并不完全相同,尽管很多时候看起来可以互换,这是因为在很多情况下,数组会退化成指向其首元素的指针,如下面的代码,就是退化机制在起作用:
const char name[] = "erpanger"; // name的类型是const char[8]
const char* ptrToName = name; // 数组会退化成指针
这里类型为const char* 的指针ptrToName 是通过name进行初始化的,而后者的类型是const char[8], 这两个类型并不统一, 就是通过了退化机制,才能编译通过
但当一个数组传递按值传递的模板时 :
template<typename T>
void f(T param);
f(name);
因为数组形参声明会按照退化成指针形参的形式,所以在按值传递给函数模板的时候,数组类型就会被推导成指针类型,即T会被推导成const char *
尽管函数无法被声明成真正的数组类型的形参,但他们却能够将形参声明成数组的引用,所以如果吧f改成引用类型的方式传参的模板:
template<typename T>
void f(T& param);
之后仍然传递一个数组,这种时候T的类型会被推导成实际的数组类型, 会包含数组的尺寸, 即:const char[8]
而f的形参,则会被推导成 const char (&)[8]
通过这种特性,我们可以创造一种模板,用来推导出数组含有的元素个数:
// 在编译期间以常量的形式返回数组的尺寸
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
注 :constexpr是在编译期间可以获得返回值的标记
int nums[] = {1,2,3,4,5,6};
int sameNums[arraySize(nums)]; // sameNums 也会被初始化为有6个元素的数组
函数实参
数组并非唯一一个会退化成指针的东西,函数类型 也会退化成函数指针,并且上面的所有对数组类型推导的讨论都适用于函数及其向函数指针的退化。
例如:
void myFunc(int, double) // 这个函数的类型为 void(int, double)
template<typename T>
void f1(T param); // f1按值传递
template<typename T>
void f2(T& param); // f2按引用传递
f1(myFunc); // param会被推导成函数指针,具体类型为void (*)(int , double)
f2(myFunc); // param会被推导成函数引用,具体类型为void (&)(int , double)
总结
- 在模板类型推导的过程中,具有引用类型的实参会被当成非引用的类型来处理
- 对万能引用形参进行推导的时候,左值实参会被特殊处理
- 对按值传递的形参进行推导时, 若实参类型中带有const或volatile, 会忽略这两个修饰词
- 模板类型推导的过程中,数组或函数的实参会退化成对应的指针,除非他们被用来初始化引用