上一节(12.2.3)讲的是"默认模板参数怎么写"(语法规则);本节讲"为什么要把一个具体类型设为默认"(工程理由)。一句话:把"最常用的那一种配置"做进默认值里,使用者只写自己关心的那个参数,其余免谈。典型例子是 template <typename T = int, int size = 10> class Array——于是 Array<> 是"10 个 int"、Array<double> 是"10 个 double"、Array<double, 20> 换成长度 20。这也是标准库能一边演进、一边不破坏老代码的秘密(vector 的第二个参数、map 的后两个参数,都是这么藏的)。
12.2.3 是"规则",本节是"用法"——两节配合起来才是完整的"默认模板参数"。本节还第一次把非类型模板参数(int size)和类型参数(T)放在一起用,12.2.5 的有界数组模板就是这套配置的直接成果。
往回收旧账:12.1.2 实例01 用过 template <typename T, int N>(那里 N 从数组长度推出来);本节让它有默认值。
一、先把两节的分工说清
| 12.2.3 默认模板参数 | 12.2.4 为具体类型的参数提供默认值(本节) | |
|---|---|---|
| 回答的问题 | 怎么写? | 为什么这么写、实际怎么用? |
| 内容 | 语法、三条铁律、和函数默认参数的区别 | 设计理由、常用配置、覆盖规则、库设计惯例 |
| 一句话 | template <typename T = int> 的规则 |
把"最常用的那一种"设成默认,让多数人少写几个字 |
换个说法:12.2.3 教你"打这个结",本节告诉你"为什么要打、打在哪儿"。
二、为什么要把"一个具体类型"设为默认
理由 1:让最常用的用法最省事
Array 这种容器,90% 的场合就是"装整数、长度 10 左右"。那就把最常用的设成默认:
template <typename T = int, int size = 10>
class Array { ... };
Array<> a; // ★ 最常用的那种:一个模板实参都不用写
Array<double> b; // 想换元素类型:只写一个
Array<double, 20> c; // 想全换:写两个
一句话:"默认值 = 给 90% 的人准备的;模板参数 = 给 10% 的特殊需求留的门。"
理由 2:让库能"长大"而不弄坏老代码
这是最值钱的一条,也是 C++ 标准库设计里反复出现的套路。
// 第 1 版:库作者只做了一个参数
template <typename T>
class Array { T data[10]; };
// 用户代码(已经存在,遍布全世界)
Array<int> a; // ✓
某天库作者想加一个"数组长度"参数。如果没有默认值:
template <typename T, int size> // ✗ 所有老代码都编不过了
class Array { T data[size]; };
有了默认值:
template <typename T, int size = 10> // ✓ 老代码 Array<int> 一行不用改
class Array { T data[size]; };
这就是"向后兼容"的机械原理:新参数一律给默认值,且默认值等于老行为。 标准库能用几十年而代码不大面积崩塌,靠的就是这个。
理由 3:它是现代库的"标准长相"
看看标准库里那些你从未关心过的参数:
| 你写的 | 它实际上是 | 藏起来的参数靠什么 |
|---|---|---|
string s; |
basic_string<char, char_traits<char>, allocator<char>> |
全部默认值 |
vector<int> v; |
vector<int, allocator<int>> |
第 2 个参数默认 |
map<string, int> m; |
map<string, int, less<string>, allocator<pair<const string,int>>> |
后两个参数默认 |
cout |
basic_ostream<char, char_traits<char>> |
默认 |
所以:"看不懂标准库的类型名"不是你的问题——那些参数的存在意义就是"让你不用管它们"。
三、非类型参数也能有默认值
默认值不只给类型参数,值参数(非类型参数)同样可以:
template <typename T = int, int size = 10>
class Array {
private:
T data[size]; // ★ size 是编译期常量,可以直接当数组长度
public:
int length() const { return size; }
...
};
两个参数,一个是类型(T),一个是值(size),都给默认值——这就是"配置化"的最小模型。
注意:
size是编译期常量(不是变量),所以T data[size];这种"用模板参数当数组长度"的写法合法。这也是非类型模板参数最重要的用途。
四、覆盖规则:只能"从右往左"覆盖
| 写法 | 等价于 | 说明 |
|---|---|---|
Array<> |
Array<int, 10> |
全默认 |
Array<double> |
Array<double, 10> |
覆盖左边那个(类型) |
Array<double, 20> |
Array<double, 20> |
全指定 |
Array<, 20> |
—— | ❌ 不允许"跳过中间"(语法上就写不出来) |
规律一句话:模板实参从左边开始填,一旦开始填,右边的就只能继续填或走默认——不能回头只改后面的。
(这与 12.2.3 铁律 1 —— "默认值必须从右往左给"——是同一条规则的两面:给默认值从右往左;填实参从左往右。)
五、动手 Demo:一个"可配置"的数组
#include <iostream>
using namespace std;
template <typename T = int, int size = 10>
class Array {
private:
T data[size];
public:
Array() {
for (int i = 0; i < size; i++) data[i] = T(); // ★ 用 T 的零值初始化(12.1.2)
}
int length() const { return size; }
void set(int i, T v) { if (i >= 0 && i < size) data[i] = v; }
T get(int i) const { return (i >= 0 && i < size) ? data[i] : T(); }
void fill(T v) { for (int i = 0; i < size; i++) data[i] = v; }
void show() const {
cout << "长度 " << size << ":";
for (int i = 0; i < size; i++) cout << data[i] << " ";
cout << endl;
}
};
int main() {
Array<> a; // ★ 一个实参都不写 → int[10]
Array<double, 5> b; // 全指定
Array<char, 8> c;
a.fill(1);
a.show();
b.fill(2.5);
b.show();
c.fill('x');
c.show();
cout << "a 的长度 = " << a.length() << ",b 的长度 = " << b.length() << endl;
a.set(3, 99);
cout << "a[3] = " << a.get(3) << ",a[100] = " << a.get(100) << "(越界返回 0)" << endl;
return 0;
}
看到什么算成功:
长度 10:1 1 1 1 1 1 1 1 1 1
长度 5:2.5 2.5 2.5 2.5 2.5
长度 8:x x x x x x x x
a 的长度 = 10,b 的长度 = 5
a[3] = 99,a[100] = 0(越界返回 0)
读结果:
| 现象 | 说明 |
|---|---|
Array<> a; 得到一个 10 个 int 的数组 |
两个默认值都生效:T = int、size = 10 |
Array<double, 5> 只改了类型和长度 |
全指定 |
三次 show() 的长度都对 |
size 是编译期常量,直接当数组长度用(这是非类型参数的核心价值) |
a.get(100) 返回 0 |
这个 Demo 里我加了简单的越界保护——12.2.5 会把它做成主角 |
30 秒实验:
- 把
Array<double, 5> b;改成Array<double> b;(只写类型)。 - 编译运行 → 它变成"长度 10"——
size走了默认值。 - 再把
Array<> a;改成Array a;(去掉尖括号)→ 编译错class template argument required。 - 两个现象合起来说明:默认值省的是"尖括号里的东西",尖括号本身省不掉。
六、常见误区
- "默认值会像函数默认参数那样自动填空位"——不会。你可以"从左往右填到某个位置",但不能跳过(
Array<, 20>语法上就写不出来)。要换后面的参数,必须把前面的也写出来。 - "默认值可以反悔、随时重给"——同一作用域只能给一次(12.2.3 铁律 2)。
- "有没有默认值无所谓,反正都要写"——关系很大:库作者靠它加新参数而不弄坏老代码。这是"向后兼容"的机械原理。
- "默认模板参数就是'让类型变灵活'"——它省的是"打字",不是"检查"。
Array<>和Array<int, 10>是同一个类(完全等价),不是"运行时什么都能变"。 - "非类型参数只能写整数"——实际上还能是指针、引用、枚举值等(C++11 后甚至能是某些常量表达式)。但最常见、最好用的就是整数(数组长度、容量上限),先把这个用熟。
- "标准库那些
allocator<char>、char_traits<char>我也得学会用"——不必。它们存在的目的正是"让你不用管它们"。看不懂那些参数是正常的。
自测三问(点开对答案)
1. `template- ☐ 12.2.5 有界数组模板(下一节)