1.1 函数模板
函数模板看起来会是这样(伪码):
template<typename T>
void f(ParamType param);
f(expr); // 用一些表达式来调用f
在编译的时候,编译器通过 expr 推导出两个类型:T 和ParamType。通常来说T和ParamType是不同的,因为 ParamType 通常包含一些类型的装饰:
template<typename T>
void f(const T& param); // ParamType 是 const T&
int x = 0;
f(x); // 使用int调用f
T 被推导成 int , ParamType 被推导成 const int& 。
1.2 T 类型推导的三种情况
对于 T 的类型推导不仅取决于 expr 的类型,还取决于 ParamType 的形式。有三种情况:
1.2.1 ParamType 是引用或指针,但不是通用引用
- 如果 expr 的类型是个引用,忽略引用的部分
- 然后利用 expr 的类型和 ParamType 对比去判断 T 的类型
template<typename T>
void f(T& param); // param是一个引用类型
int x = 27; // x是一个int
const int cx = x; // cx是一个const int
const int& rx = x; // rx是const int的引用
f(x); // T是int,param的类型是int&
f(cx); // T是const int,param的类型是const int&
f(rx); // T是const int,param的类型是const int&
如果 f 的参数类型从 T& 变成 const T& ,情况就会发生变化:
template<typename T>
void f(const T& param); // param现在是const的引用
int x = 27; // 和之前一样
const int cx = x; // 和之前一样
const int& rx = x; // 和之前一样
f(x); // T是int,param的类型是const int&
f(cx); // T是int,param的类型是const int&
f(rx); // T是int,param的类型是const int&
如果 param 是一个指针(或者指向 const 的指针)而不是引用,情况也是类似:
template<typename T>
void f(T* param); // param是一个指针
int x = 27; // 和之前一样
const int *px = &x; // px是一个指向const int x的指针
f(&x); // T是int,param的类型是int*
f(px); // T是const int,param的类型时const int*
1.2.2 ParamType 是通用引用(Universal Reference)
通用引用参数的类型声明是 T&& ,对于通用的引用参数,情况就变得不是那么明显了。
- 如果 expr 是一个左值, T 和 ParamType 都会被推导成左值引用。
- 如果 expr 是一个右值,那么就执行“普通”的法则(第一种情况)。
template<typename T>
void f(T&& param); // param现在是一个通用的引用
int x = 27; // 和之前一样
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&&
1.2.3 ParamType 既不是指针也不是引用
按pass-by-value处理:
- 如果 expr 的类型是个引用,将会忽略引用的部分。
- 如果expr 是 const 的,也要忽略掉 const 。如果是 volatile的,也要忽略掉。
template<typename T>
void f(T param); // param现在是pass-by-value
int x = 27; // 和之前一样
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 不能被修改和 param 能不能被修改是没有关系的
指针自己( ptr )本身是被按值传递,则忽略指针本身的const:
template<typename T>
void f(T param); // param仍然是按值传递的(pass by value)
const char* const ptr = "Fun with pointers";// ptr是一个const指针,指向一个const对象
f(ptr); // 给参数传递的是一个const char * const类型
//param 推导出来的类型就是 const char*
1.3 数组参数
虽然通常情况下,一个数组会被退化成一个指向其第一个元素的指针,数组类型和指针类型是不一样的:
const char name[] = "J. P. Briggs"; // name的类型是const char[13]
const char * ptrToName = name; // 数组被退化成指针
//(const char* 和 const char[13] 是不一样的类型,但基于数组到指针的退化规则,代码会被正常编译
模板拥有一个按值传递的参数,不能将数组作为函数参数。虽然,下面这样是合法的。
//数组声明被当作指针声明来处理
template<typename T>
void f(T param); // 模板拥有一个按值传递的参数
void myFunc(int param[]);
void myFunc(int* param); // 和上面的函数是一样的
f(name); // name是个数组,但是T被推导成const char*
可以声明参数是数组的引用:
template<typename T>
void f(T& param); // 引用参数的模板
f(name); // 传递数组给f
// T 被推导成了 const char [13] ,函数 f 的参数(数组的引用)被推导成了 const char(&)[13]
通过声明数组的引用,可以创造出推导数组长度的模板:
// 在编译的时候返回数组的长度(数组参数没有名字,因为只关心数组包含的元素的个数)
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // keyVals有7个元素
int mappedVals[arraySize(keyVals)]; // mappedVals长度也是7
数组的替代方案:
std::array<int, arraySize(keyVals)> mappedVals; // mappedVals长度是7
1.4 函数参数
函数类型可以被退化成函数指针:
void someFunc(int, double); // someFunc是一个函数,类型是void(int, double)
template<typename T>
void f1(T param); // 在f1中 参数直接按值传递
template<typename T>
void f2(T& param); // 在f2中 参数是按照引用传递
f1(someFunc); // param被推导成函数指针,类型是void(*)(int, double)
f2(someFunc); // param被推导成函数引用,类型是void(&)(int, double)
1.5 要点速记
- 在模板类型推导期间,引用实参被视为非引用,即忽略其引用性质。
- 在推导通用引用形参的类型时,左值实参会得到特殊处理。
- 在推导按值传递的形参类型时,const 和/或 volatile 参数被视为非 const 和非volatile的。
- 在模板类型推导期间,数组或函数实参会退化为相应的指针,除非它们用于初始化引用。