在 C++ 中,编写模板时写在 template <...> 尖括号里面的参数,统称为 “模板参数(template parameter)”。前面我们接触的模板大多只声明了单个 typename T,但在实际开发中,模板参数还有更灵活的写法。
比如同一个模板里可能包含多个独立变化的数据类型,有时需要传入一个常量整数来指定数组的固定长度,还有时希望给模板参数设置默认值。围绕这些需求,C++ 提供了 “类型模板参数”、“非类型模板参数” 和 “默认模板参数”。
C++ 类型模板参数
用来代表某一种待定数据类型的模板参数,称为 “类型模板参数”。我们可以拿普通函数的参数来对比:定义模板时写在 template <...> 里的 T 叫做 “模板形参”,使用时填入尖括号里的 int、std::string 等具体类型叫做 “模板实参”。
1. typename 与 class 的区别
在声明类型模板参数时,C++ 支持使用 typename 或 class 关键字,下面 2 种方式是完全等价的:
// 方式 1(推荐)
template <typename T>
void show(T value) {
// 函数体
}
// 方式 2
template <class T>
void show(T value) {
// 函数体
}对于上面这两种写法,小伙伴们只需要清楚以下 2 点:
- class T 不代表只能传类:很多初学者看到
template <class T>,容易误以为后面只能传入自定义的类。实际上并非如此,int、double、指针等任何数据类型都可以传给class T,这里的class只是早期 C++ 沿用下来的关键字。 - 推荐统一使用 typename:从字面意思来看,
typename表示 “类型名称”,一眼就能看出这是一个类型参数,也不会和定义类的class混淆,因此在实际开发中我们更推荐使用方式 1。
2. 多类型模板参数
前面我们遇到过一个限制:如果模板只声明了一个 typename T,那么调用时直接传入 int 和 double 两个不同类型的实参就会推导失败。
实际上,一个模板并不是只能声明一个类型参数。如果函数模板或类模板中有两个甚至多个彼此独立变化的数据类型,我们可以在尖括号里使用英文逗号(,)隔开,声明多个类型参数。
语法:
// 多类型函数模板
template <typename T, typename U>
返回类型 函数名(T 参数1, U 参数2) {
// 函数体
}
// 多类型类模板
template <typename K, typename V>
class 类名 {
// 成员变量与成员函数
};说明:
在上面的语法中,T 和 U 是两个各自独立的类型参数,在表示键值对时也常写成 K 和 V。使用时,它们既可以接收不同的数据类型,也可以接收相同的数据类型。
示例 1:多类型参数的函数模板
#include <iostream>
template <typename T, typename U>
void printPair(const T& left, const U& right) {
std::cout << left << " : " << right << '\n';
}
int main() {
printPair("Jack", 18);
printPair(1001, 95.5);
printPair(10, 20);
return 0;
}运行结果如下。
Jack : 18
1001 : 95.5
10 : 20分析:
在这个例子中,printPair() 声明了 T 和 U 两个独立的类型参数。第一次调用传入字符串和 int,第二次调用传入 int 和 double,编译器会根据左右两个实参各自推导出 T 和 U 的类型,互不干扰。
如果像第三次调用那样传入两个 int 整数,T 和 U 也可以同时被推导为 int。多类型模板参数同样常用于类模板中,比如编写一个用来保存 “键(Key)” 和 “值(Value)” 的键值对类模板。下面来看一个例子。
示例 2:多类型参数的类模板
#include <iostream>
#include <string>
template <typename K, typename V>
class KeyValue {
private:
K key;
V value;
public:
KeyValue(const K& k, const V& v) {
key = k;
value = v;
}
void show() const {
std::cout << key << " -> " << value << '\n';
}
};
int main() {
KeyValue<std::string, int> item1("score", 95);
item1.show();
KeyValue<int, std::string> item2(1001, "Lucy");
item2.show();
return 0;
}运行结果如下。
score -> 95
1001 -> Lucy分析:
在这个例子中,KeyValue<std::string, int> 将第 1 个参数 K 指定为 std::string,将第 2 个参数 V 指定为 int。而创建 item2 时则反过来,将 K 指定为 int,将 V 指定为 std::string。
C++ 非类型模板参数
前面介绍的 T、U、K、V 代表的都是 “某一种数据类型”。除此之外,模板参数还可以是一个在编译阶段就已经确定的常量整数,常用来指定数组的固定长度或容量上限,这种参数称为 “非类型模板参数”。
语法:
template <typename T, int N>
class 类名 {
// 可将 N 当作常量整数使用
};说明:
在 template <typename T, int N> 中,typename T 声明的是类型参数,而 int N 声明的则是一个整数类型的非类型模板参数。由于数组长度不会为负数,在标准库中也常将其声明为无符号整型 size_t N。
进入类模板内部后,N 就是一个现成的编译期常量,可以直接拿来定义固定长度的数组 T data[N]。在实际开发中,非类型模板参数最常用的就是 int、size_t 等整数类型,不能把 std::string 字符串这类对象当成非类型模板参数传进去。
示例 3:使用非类型模板参数
#include <iostream>
#include <string>
template <typename T, int N>
class FixedArray {
private:
T data[N] = {};
public:
void set(int index, const T& value) {
if (index >= 0 && index < N) {
data[index] = value;
}
}
T get(int index) const {
return data[index];
}
int size() const {
return N;
}
};
int main() {
FixedArray<int, 4> intArr;
intArr.set(0, 10);
std::cout << intArr.size() << ", " << intArr.get(0) << '\n';
FixedArray<std::string, 3> strArr;
strArr.set(0, "C++");
std::cout << strArr.size() << ", " << strArr.get(0) << '\n';
return 0;
}运行结果如下。
4, 10
3, C++分析:
FixedArray<int, 4> intArr; 表示把类型参数 T 指定为 int,同时把非类型参数 N 指定为整数 4。编译器在实例化时,会把类内部的 T data[N] = {}; 生成为 int data[4] = {};,并将数组元素全部初始化为默认值 0。
在使用非类型模板参数时,小伙伴们需要注意以下 2 条规则:
- 实参必须是编译期常量:由于模板是在编译阶段生成具体代码的,传给
N的实参必须在编译阶段就能确定数值,比如整数字面量 4,或者直接用字面量初始化的const整型常量,不能传入运行期间才确定值的普通变量。 - 非类型实参不同也属于不同的类:比如
FixedArray<int, 3>和FixedArray<int, 4>,虽然元素类型都是int,但因为长度实参 3 和 4 不同,编译器会将它们生成为两个完全独立的类,彼此不能直接赋值。
比如在下面的代码中,len 是一个普通整型变量,它的值在运行阶段才确定,作为非类型模板实参传入时就会报错:
int len = 5;
FixedArray<int, len> arr; // 编译报错C++ 默认模板参数
和普通函数支持默认参数类似,C++ 的模板参数也支持通过等号(=)指定默认的数据类型或默认整数值,称为 “默认模板参数”。
语法:
template <typename T = 默认类型, int N = 默认整数>
class 类名 {
// 成员变量与成员函数
};说明:
在包含多个参数的类模板中,一旦某个模板参数设置了默认值,排在它右边的所有模板参数也都必须设置默认值,不能左边有默认值而右边没有默认值:
// 正确方式
template <typename K, typename V = int>
class PairBox {};
// 错误方式
template <typename K = int, typename V>
class PairBox {};创建类模板对象时,如果显式填入具体的模板实参,新传入的实参就会覆盖默认值。如果所有模板参数都设有默认值,我们想直接使用默认配置,只需在类名后面写一对空尖括号 类名<> 对象名; 就可以了。
示例 4:指定默认模板参数
#include <iostream>
#include <string>
// 指定默认模板参数
template <typename T = int, int N = 10>
class BufferBox {
private:
T data[N] = {};
public:
BufferBox(const T& firstVal) {
data[0] = firstVal;
}
void showInfo() const {
std::cout << data[0] << ",容量:" << N << '\n';
}
};
int main() {
// 全部使用默认参数
BufferBox<> box1(100);
box1.showInfo();
// 只指定第 1 个参数
BufferBox<std::string> box2("绿叶网");
box2.showInfo();
// 覆盖全部默认参数
BufferBox<double, 50> box3(3.14);
box3.showInfo();
return 0;
}运行结果如下。
100,容量:10
绿叶网,容量:10
3.14,容量:50分析:
在这个例子中,BufferBox 将类型参数 T 的默认值设为 int,将容量参数 N 的默认值设为 10。创建 box1 时写了空尖括号 BufferBox<>,等价于 BufferBox<int, 10>,两个参数全部采用默认值。
创建 box2 时只传入了 std::string,第 1 个参数 T 被替换为 std::string,省略的第 2 个参数 N 继续使用默认值 10。创建 box3 时传入了 double 和 50,两个默认值则被全部覆盖。
这里小伙伴们要留意:就像调用带默认参数的普通函数仍然要写空小括号 func() 一样,使用类模板的全部默认参数创建对象时,推荐在类名后面保留一对空尖括号写成 BufferBox<> box1(100);,这样一眼就能看出 BufferBox 是一个类模板。
提示 函数模板同样支持设置默认模板参数,比如 template <typename T = int> void show(T a = 0)。与类模板不同的是,调用函数模板时如果全部使用默认参数,直接写 show() 即可,不需要加上空尖括号 “<>”。
