模板是C++的重要特性,是C++标准模板库的基础。模板可以根据数据类型自动生成代码,大大减少重复代码。模板实例化的时候编译器需要根据具体变量推导数据类型,模板推导出的类型很多时候是显而易见的,有些时候却不太明显,本文详细阐述一下C++模板的类型推导机制。

在C++中声明一个模板函数的伪代码如下:

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

上面的ParamTypeT不一定相同,比如:

1
2
template<typename T>
void f(const T& param);

此时ParamType的类型是const T&

调用模板函数方式如下:

1
f(expr);

编译的时候,编译器通过表达式expr推导出两个类型ParamTypeT

模板的类型推导与ParamType密切相关,根据ParamType的类型可以分为三种情形:

  1. ParamType 既不是指针也不是引用。
  2. ParamType 是指针或引用,不是通用引用。
  3. ParamType 是通用引用。

下面分别讨论一下三种情形:

ParamType 既不是指针也不是引用

ParamTypeT相同,此时模板如下:

1
2
template<typename T>
void f(T param);

这种情况下参数param的类型T可以理解为值传递param会复制一份expr)时的类型。这意味着:

  • 如果expr是一个引用,忽略引用部分。
  • 如果expr带有constvolatile,忽略constvolatile

举个例子:

1
2
3
4
5
6
int x = 27;
const int cx = x;
const int& rx = x;
f(x);
f(cx);
f(rx);

上面的三种调用方式,T的类型都是int

需要注意下面一种情况:

1
2
const char* const ptr = "Fun with pointers";
f(ptr);

ptr是指向字符串常量的常量指针,此时进行值传递T的类型应为const char*。因为:

  1. 开头的const修饰的是指向的对象,表示指向的字符串不可变,不可忽略,否则指向的类型就不对了。
  2. *右边的const修饰的是指针,表示指针本身不可变,在值传递的情况下指针是被复制一份,该const没有意义。

ParamType 是指针或引用,不是通用引用

这种情况下,推导步骤如下:

  1. 如果expr是一个引用,忽略引用部分。
  2. expr类型与ParamType进行模式匹配,先确定ParamType,再根据ParamType推导T

举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<typename T>
void f(T& param);  // 参数是引用类型

int x = 27;
const int cx = x;
const int& rx = x;

f(x);  // x是int类型,ParamType类型是int&,所以T是int类型
f(cx); // cx是const int类型,ParamType类型是const int&,所以T是const int类型
f(rx); // rx是const int&类型,忽略引用部分,同cx,ParamType类型是const int&,所以T是const int类型

如果把参数类型改为const T&,相应的T的类型也会有一点变化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<typename T>
void f(const T& param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x);  // x是int类型,ParamType类型是const int&,所以T是int类型
f(cx); // cx是const int类型,ParamType类型是const int&,所以T是int类型
f(rx); // rx是const int&类型,忽略引用部分,同cx,ParamType类型是const int&,所以T是int类型

如果参数是指针类型,也类似:

1
2
3
4
5
6
7
8
template<typename T>
void f(T* param);  // 参数是指针类型

int x = 27;
const int *px = &x;

f(&x); // &x是int*类型,ParamType类型是int*,所以T是int类型
f(px); // px是const int*类型,ParamType类型是const int*,所以T是const int类型

ParamType 是通用引用

这种情况下,推导规则如下:

  • 如果expr是左值,那么TParamType都是左值引用。这条规则很特殊:首先,这是唯一一种T被推导为引用类型的情形;其次,ParamType的声明形式是右值引用的语法,但是实际类型为左值引用。
  • 如果expr是右值,推导规则与普通引用一致。

举例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
template<typename T>
void f(T&& param); // 参数是通用引用

int x = 27;
const int cx = x;
const int& rx = x;

f(x);  // x 是左值,所以T和ParamType都是int&类型
f(cx); // cx 是左值,所以T和ParamType都是const int&类型
f(rx); // rx 是左值,所以T和ParamType都是const int&类型
f(27); // 27 是右值,ParamType是int&&类型,所以T是int类型

以上三种情形就是C++模板类型推导的全部规则了。下面简单补充说明一下C++里两种特殊的参数类型:数组参数和函数参数。

数组参数

在C语言和C++里,如下的两个函数声明是完全等价的:

1
2
void myFunc(int param[]);
void myFunc(int* param);

即函数的数组参数会被当成指针参数

如果要使用真正的数组参数,在C++中可以使用数组引用类型:

1
void myFunc(int (&param)[5]); // 使用数组引用时必须指定数组大小,同定义数组一样

int (&param)[5]表示一个大小为5的整数数组引用类型。

了解数组参数的以上特点后,可以很容易的理解模板如何推导数组类型的参数。举两个例子:

1
2
3
4
5
template<typename T>
void f(T param);

const char name[] = "J. P. Briggs";
f(name); // name是数组,param是值传递, 数组参数当成指针参数,因此param的类型是const char*
1
2
3
4
5
template<typename T>
void f(T& param);

const char name[] = "J. P. Briggs";
f(name); // name是数组,param是引用类型,因此param的类型是const char(&)[13]

函数参数

除了数组参数,函数参数也会被当成函数指针。函数参数的类型推导规则跟数组参数完全一样。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void someFunc(int, double);

template<typename T>
void f1(T param);

template<typename T>
void f2(T& param);

f1(someFunc); // param是值传递, 函数参数当成函数指针,因此param的类型是void (*)(int, double)
f2(someFunc); // param是引用类型,因此param的类型是void (&)(int, double)

实际使用中函数指针和函数引用基本没有区别。两者调用时都可以解引用或直接调用,唯一的区别是引用初始化时只能用函数名称,不能在前面加&。

1
2
3
4
5
void (*pf)(int) = someFunc; // 也可以写成 void (*pf)(int) = &someFunc;
void (&rf)(int) = someFunc;

pf(8, 1.2); // 也可以写成 (*pf)(8, 1.2);
rf(8, 1.2); // 也可以写成 (*rf)(8, 1.2);

总结:

  • ParamType 既不是指针也不是引用时,采用值传递模式,忽略表达式的引用部分、const、volatile。
  • ParamType 是指针或引用,不是通用引用时,忽略引用部分,进行模式匹配,先确定ParamType,再推导T。
  • ParamType 是通用引用时,左值特殊对待(T和ParamType都是左值引用)。
  • 数组参数、函数参数非引用传递时当作指针,引用类型不会。