条款1:模板类型推导
模板类型推导根据实参推断模板参数,自动处理引用、指针及cv限定,实现类型匹配与转换。
条款1:模板类型推导
模板类型推导统一步骤:先忽略实参引用性,再根据形参模板形式不同决定是否保留 cv 限定和是否允许
T
推导成引用类型。“忽略实参引用性”是所有模板类型推导的基础预处理规则。
四个要素
函数模板大致形如:
1
2
3
4
template<typename T>
void f(ParamType param);
// 以某种表达式调用 f
f(expr);
在编译期间,编译器使用expr
进行两个类型推导:一个是针对T
的,另一个是针对ParamType
的。这两个类型通常是不同的,因为ParamType
包含一些修饰,比如const
和引用修饰符。
一共涉及到四个要素:T
、ParamType
、param
、expr
,它们分别在模板类型推导中扮演不同角色。
T
— 模板参数(Template Parameter)
这是函数模板的 模板参数类型,由编译器根据调用 f(expr)
时传入的实参 expr
自动推导得出。
- 是一个占位符,用于表示某种类型。
- 类型推导的目标就是确定
T
是什么类型。
ParamType
— 形参类型(Parameter Type)
这是函数 f
的形参的声明类型,是由 T
派生出来的一个形式类型。
- 可以是
T
、T&
、const T&
、T&&
、T*
等等。 ParamType
的形式决定了 类型推导的规则和结果。- 它也可以是
auto
,那就变成了 C++14 的自动类型推导。
例子:
1
2
template<typename T>
void f(const T& param);
此时 ParamType
是 const T&
,类型推导的时候要从 const T&
的角度去分析 expr
。
param
— 形参名(Parameter Name)
就是 f
函数里定义的 形参变量名,是实际在函数体内使用的参数。
- 和普通函数参数一样,用于在函数中访问调用者传进来的数据。
- 对类型推导无直接影响,属于语法形式上的变量命名。
expr
— 实参表达式(Argument Expression)
这就是调用函数 f(expr)
时传进去的实际表达式,用于参与类型推导。
- 编译器根据
expr
的类型 +ParamType
的形式来反推T
是什么。 - 不同的
ParamType
形式会导致推导出的T
不一样(这是类型推导的核心)。
第一类:形参是指针或引用类型,但不是万能引用
对应函数形参形如:T&
、const T&
、T*
、const T*
等,不包括 T&&
(即第二类)。
这种情况下,类型推导会这样运作:
若
expr
具有引用类型,则首先忽略引用部分然后对
expr
的类型和ParamType
的类型执行模式匹配,决定T
的类型
示例 1:
1
2
3
4
5
6
7
8
9
10
template<typename T>
void f(T& param); // param 是个引用
int x = 996; // x 类型为 int
const int cx = x; // cx 的类型为 const int
const int& rx = x; // rx 是一个 const int 类型的引用,引用了 int 类型的 x
f(x); // T = int, param 类型是 int&
f(cx); // T = const int, param 类型是 const int&
f(rx); // T = const int, param 类型是 const int&
第二个和第三个调用,向引用类型的形参传入 const
对象时,自然是希望保持其不可修改的属性,也就是期望该形参成为 const
的引用类型。该对象的常量性 constness
会成为 T
的类型推导结果的组成部分。
第三个调用中,rx
的引用性会在类型推导过程中被忽略。
这个例子只展示了左值引用,但是类型推导会如左值引用一样对待右值引用。当然,右值只能传递给右值引用,但是在类型推导中这种限制将不复存在。
示例 2:
将 f
的形参类型 T&
改为 const T&
,情况有所变化,但不会变得那么出人意料,cx
和 rx
的常量性 constness
依然会被保留。因为现在我们假设 param
是 reference-to-const
,所以 const
不再被推导为T
的一部分:
1
2
3
4
5
6
7
8
9
10
template<typename T>
void f(const T& param); // param 是一个 const T 类型的引用
int x = 27; // x 的类型是 int
const int cx = x; // cx 的类型是 const int
const int& rx = x; // rx 是一个 const int 类型的引用,引用了 x
f(x); // T = int, param 的类型是 const int&
f(cx); // T = int, param 的类型是 const int&
f(rx); // T = int, param 的类型是 const int&
和之前一样,rx
的引用性会在类型推导过程中被忽略。
示例 3:
如果param
是一个指针(或者指向const
的指针)而不是引用,情况本质上也一样:
1
2
3
4
5
6
7
8
9
10
template<typename T>
void f(T* param); // param 是个指针,指向类型为 T 的对象
int x = 27; // x 的类型是 int
const int* px = &x; // px 是个指针,指向 const int 类型的对象(x)
const int* const qx = &x; // qx 是个 const 指针,指向 const int 类型的对象(x)
f(&x); // T = int, param 的类型是 int*
f(px); // T = const int, param 的类型是 const int*
f(qx); // T = const int, param 的类型是 const int*
模板类型推导时会忽略实参的顶层 const,但会保留底层 const。所以 const int* const qx
传给 T* param
,推导结果是 T = const int
,param 是 const int*
。换句话说:“指针本身是 const”不会影响 T 的推导;“指针指向的东西是 const”会体现在 T 中。
小结
第一类推导规则:当函数形参是指针或引用类型,但不是万能引用(即形参形如 T&
、const T&
、T*
)时的推导规则。
- T 的推导 不会保留引用性,但会 保留 cv 修饰符。
- 实参如果是引用,会先移除引用再推导。
- 指针类型按被指向对象类型推导 T,保留其 const / volatile。
- 不会发生退化,数组 / 函数会保持原类型。
第二类:形参是万能引用(Universal Reference)
实参如果是引用,推导时忽略引用部分。
如果实参是左值,
则
T
被推导为T&
(左值引用),ParamType
最终变成T& &&
,折叠成T&
。这是模板中唯一
T
会被推导成引用的情况。
如果实参是右值,则
T
正常推导为实参类型,ParamType
保持为T&&
。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
template<typename T>
void f(T&& param); // param 是一个万能引用(Universal Reference)
int x = 27; // x 的类型是 int
const int cx = x; // cx 的类型是 const int
const int& rx = cx; // rx 是 const int 的左值引用
f(x); // x 是左值,所以 T = int&,
// param 的类型是 int&(T& && 折叠为 T&)
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&&(原样保留)
int* p = &x; // p 是 int* 类型
const int* cp = &cx; // cp 是 const int* 类型(指向 const 对象)
const int* const ccp = &cx; // ccp 是 const 指针,指向 const 对象
f(p); // p 是左值,类型是 int*,
// 所以 T = int*&,param 类型是 int*&
f(cp); // cp 是左值,类型是 const int*,
// 所以 T = const int*&,param 类型是 const int*&
f(ccp); // ccp 是左值,类型是 const int* const,
// 所以 T = const int* const&,param 类型是 const int* const&
f(new int(42)); // new int(42) 是右值,类型是 int*
// 所以 T = int*,param 类型是 int*&&(原样保留)
f(static_cast<const int*>("hello"));
// 是右值,类型为 const int*
// 所以 T = const int*,param 类型是 const int*&&(原样保留)
第三类:形参既非指针/引用,也非万能引用
当形参既不是指针也不是引用时,我们通过传值(pass-by-value)的方式处理:
1
2
template<typename T>
void f(T param); // 以传值的方式处理 param
这意味着无论传递什么 param
都会成为它的一份拷贝——一个完整的新对象。事实上 param
成为一个新对象这一行为会影响 T
如何从 expr
中推导出结果。
实参如果是引用,推导时忽略引用部分。
忽略引用后,如果实参是顶层
const
或volatile
,也会被忽略。
示例 1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
void f(T param); // param 以值传递方式接收,类型是 T
int x = 27; // x 的类型是 int
const int cx = x; // cx 的类型是 const int
const int& rx = cx; // rx 是 const int 类型的左值引用
f(x); // x 是 int 类型,
// T = int,param 的类型是 int
f(cx); // cx 是 const int,传值时忽略顶层 const,
// T = int,param 的类型是 int
f(rx); // rx 是 const int&,传值时忽略引用和顶层 const,
// T = int,param 的类型是 int
即使 cx
和 rx
是 const
,param
不是 const
,因为它是它们的拷贝。cx
和 rx
的常量性不影响拷贝的可修改性,所以推导时会忽略实参的 const
(和 volatile
),确保拷贝是可变的。
示例 2:
1
2
3
4
5
6
7
8
9
10
template<typename T>
void f(T param); // param 以传值方式接收,类型是 T
const char* const ptr = "巴巴博一.mp4"; // ptr 是 const 指针,指向 const char (字符串字面量)
f(ptr); // 实参类型是 const char* const,
// 传值时顶层 const(指针本身的 const)被忽略,
// 所以 T = const char*,保留了底层 const
// param 的类型是 const char*(可变指针,指向 const 字符串)
传值时,实参的顶层 const
(比如指针本身是 const
)会被忽略,但底层 const
(指针指向的数据是 const
)会被保留。
例如,const char* const ptr
传给 T param
时,param
的类型是 const char*
,指针指向的数据不可改,但指针本身可变,因为指针本身的顶层 const
被忽略了。
对 顶层 const/volatile 和 底层 const/volatile 的保留与忽略
形参形式 | 顶层 const/volatile | 底层 const/volatile | 说明与举例 |
---|---|---|---|
T& param (引用) | 引用没有顶层 const | 保留 | 顶层 const/volatile 传递给模板参数 T ,比如 const int& 推导为 T = const int 。 |
T* param (指针) | 忽略 | 保留 | 指针本身(顶层)的 const/volatile 忽略,指针指向的类型(底层)保留。示例:const char* const ptr 传给 T* param ,T = const char 。 |
T&& param (万能引用) | 保留 | 保留 | 同引用,且根据实参值类别推导出 T ,左值时 T 会是引用类型,保持 const/volatile。 |
T param (传值) | 忽略 | 保留 | 顶层 const/volatile 会被剥除,比如 const int → int ;指针的底层 const 保留。示例:const char* const ptr 传给 T param ,T = const char* 。 |
- 顶层 const/volatile:修饰变量本身,比如指针是 const。
- 底层 const/volatile:修饰指针或引用指向的对象。
数组实参
数组类型和指针类型看起来类似,但在模板类型推导中它们是不同的。虽然数组在很多场合会退化为指针(比如传参时),但这种退化不是总发生,具体要看形参类型。正是这种“自动退化”,让人误以为数组和指针可以完全互换,其实并非如此。
数组退化示例
这种退化允许代码像下面这样正常编译:
1
2
3
4
5
6
void func(const int* p); // 参数类型是 const int*,指向 const int 的指针
int arr[5] = {1, 2, 3, 4, 5}; // arr 的实际类型是 int[5](长度为 5 的 int 数组)
func(arr); // arr 作为实参时会发生退化:int[5] → int*
// 然后 int* 被隐式转换为 const int* 以匹配函数参数类型
但在模板类型推导中是否退化,取决于形参是传值、指针、引用还是万能引用。数组作为实参,只有传值时会发生退化,传引用则保持原始数组类型。
函数数组形参实质是指针
虽然可以这样声明函数:
1
void myFunc(int param[]);
但它其实等价于:
1
void myFunc(int* param); // 数组参数视作指针参数
这是从 C 语言继承下来的特性,容易让人误以为数组和指针是等价的,但它们在类型系统中是不同的,只有在传值时会自动退化为指针。
模板传值时的数组退化
1
2
3
4
5
6
7
8
template<typename T>
void f(T param); // param 是传值参数
const char name[] = "hello";
f(name); // name 是 const char[6](数组)
// 但传值时数组会退化为指针:const char[6] → const char*
// 所以 T = const char*,param 的类型也是 const char*
传引用时数组类型不退化
虽然函数不能声明形参为真正的数组,但可以接受指向数组的引用。
例如:
1
2
3
4
5
template<typename T>
void f(T& param); // 传引用形参的模板
const char name[] = "Hello, world";
f(name); // 传数组给 f
此时 T
被推导为真正的数组类型,包含大小:
T = const char[13]
param
类型是const char (&)[13]
(引用类型,指向数组)
这种写法虽然复杂,但能保留数组的完整类型信息。
利用数组引用推导数组大小
我们可以声明一个接受数组引用的模板函数,在编译期间返回数组大小:
1
2
3
4
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept {
return N;
}
这样就能在编译期获取数组的大小。示例:
1
2
int keyVals[] = {1, 3, 7, 9, 11, 22, 35}; // keyVals 有 7 个元素
int mappedVals[arraySize(keyVals)]; // mappedVals 也有 7 个元素
作为现代 C++ 程序员,更推荐用 std::array
:
1
std::array<int, arraySize(keyVals)> mappedVals; // mappedVals 大小为 7
constexpr
保证函数在编译时执行,noexcept
有助于编译器优化,具体细节可参考相关资料。
函数实参
在 C++ 中,不仅数组会退化为指针,函数类型同样会退化为函数指针。数组类型的推导和退化规则,同样适用于函数类型。
1
2
3
4
5
6
7
8
9
10
void someFunc(int, double); // someFunc 是函数,类型为 void(int, double)
template<typename T>
void f1(T param); // 传值
template<typename T>
void f2(T& param); // 传引用
f1(someFunc); // param 被推导为函数指针类型:void(*)(int, double)
f2(someFunc); // param 被推导为函数引用类型:void(&)(int, double)
在模板类型推导中,数组名或函数名实参通常会退化为指针,除非用来初始化引用时才保留其原始类型。