01 理解模板类型推导

01 理解模板类型推导

1.1 函数模板与衍生

​ 下面是一个模板函数的代码,其中ParamType是一个与T有关的类型。

template<typename T>
void f(ParamType param);

(Easy)第一种情况: ParamType 是个非通用的引用或者是一个指针

1. T&

​ 函数模板如下:

template<typename T>
void f(T& 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&

2. const T&

​ 函数模板如下:

template<typename T>
void f(const T& 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的类型时const int&

f(cx); // T是int,param的类型是const int&

f(rx); // T是int,param的类型时const int&

3. T*

​ 函数模板如下:

template<typename T>
void f(T* param);

​ 声明的变量如下:

int x = 27; 			// x是一个int
const int* cx = &x; // cx是一个const int*

​ 调用产生的模板参数为:

f(x); 	// T是int,param的类型时int*

f(cx); // T是const int,
// param的类型是const int*

第二种情况: ParamType 是个万能引用(Universal Reference)

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&&

第三种情况: ParamType 既不是指针也不是引用

1. 传递值和引用

​ 当 ParamType 既不是指针也不是引用,我们把它处理成pass-by-value

template<typename T> 
void f(T param); // param现在是pass-by-value

​ 即,不论传递的是引用还是值,都会将值拷贝一份,并忽略cv限定符

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

2. 传递指针

指针的特殊情况

​ 指针被传递时,指针本身依旧会发生拷贝,即失去cv属性并拷贝一个新的指针,但是指针指向的内存对应的属性会被保留

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类型
// 推导结果为T = const char*

​ 按照按值传递的类型 推导法则, ptrconst 特性会被忽略,这样 param 的推导出来的类型就是 const char* , 也就是一个可以被修改的指针,指向一个 const 的字符串。 ptr 指向的东西的 const 特性被加以保留,但是 ptr 自己本身的 const 特性会被忽略,因为它要被重新复制一份而创建了一个新的指针 param

3. 传递数组(important)

3.1 正常引入

​ 使用正常的数组作为参数:

const char name[] = "J. P. Briggs"; // name的类型是const char[13]

void myFunc(int param[]); // 等价于void myFunc(int* param);
myFunc(name);

​ 结果是,数组退化为了指针参数

3.2 模板参数

​ 因为数组参数声明会被当做指针参数,传递给模板函数的按值传递的数组参数会被退化成指针类型。这就意味着在模板 f 的调用中,模板参数 T 被推导成 const char*

const char name[] = "J. P. Briggs"; // name的类型是const char[13]

template<typename T>
void f(T param); // 模板拥有一个按值传递的参数

f(name); // const char*
3.3 如果是T&(important)

​ 尽管函数不能被真正的定义成参数为数组,但是可以声明参数是数组的引用!(例如:int (&a)[10]

​ 所以如果我们修改模板 f 的参数成引用:

template<typename T>
void f(T& param); // 引用参数的模板

​ 然后传一个数组给他:

f(name); 		// 传递数组给f,T = const char (&)[13]

​ T 最后推导出来的实际的类型就是数组!类型推导包括了数组的长度,所以在这个例子里面, T 被推导成了 const char [13] ,函数 f 的参数(数组的引用)被推导成了 const char (&)[13] 。是的,语法看起来怪怪的,但是理解了这些可以升华你的精神。

3.4 数组引用T& 和 带数组长度N的模板(important)

​ 有趣的是,声明数组的引用可以创造出一个推导出一个数组包含的元素长度的模板:

// 在编译的时候返回数组的长度(数组参数没有名字,
// 因为只关心数组包含的元素的个数)
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N; // constexpr和noexcept在随后的条款中介绍
}

int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // keyVals有七个元素
int mappedVals[arraySize(keyVals)]; // mappedVals长度也是七

备注:优先使用std::array

std::array<int, arraySize(keyVals)> mappedVals; 	// mappedVals长度是七

4. 传递函数

​ 数组并不是C++唯一可以退化成指针的东西。函数类型可以被退化成函数指针,和我们之前讨论的数组的推导类似,函数可以被退化成函数指针:

void someFunc(intdouble); 	// 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)