🔷 第十二章 模板
📄 一句话概括

上一节(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 秒实验:

  1. 把 Array<double, 5> b; 改成 Array<double> b;(只写类型)。
  2. 编译运行 → 它变成"长度 10"——size 走了默认值。
  3. 再把 Array<> a; 改成 Array a;(去掉尖括号)→ 编译错 class template argument required。
  4. 两个现象合起来说明:默认值省的是"尖括号里的东西",尖括号本身省不掉。

六、常见误区

  1. "默认值会像函数默认参数那样自动填空位"——不会。你可以"从左往右填到某个位置",但不能跳过(Array<, 20> 语法上就写不出来)。要换后面的参数,必须把前面的也写出来。
  2. "默认值可以反悔、随时重给"——同一作用域只能给一次(12.2.3 铁律 2)。
  3. "有没有默认值无所谓,反正都要写"——关系很大:库作者靠它加新参数而不弄坏老代码。这是"向后兼容"的机械原理。
  4. "默认模板参数就是'让类型变灵活'"——它省的是"打字",不是"检查"。Array<> 和 Array<int, 10> 是同一个类(完全等价),不是"运行时什么都能变"。
  5. "非类型参数只能写整数"——实际上还能是指针、引用、枚举值等(C++11 后甚至能是某些常量表达式)。但最常见、最好用的就是整数(数组长度、容量上限),先把这个用熟。
  6. "标准库那些 allocator<char>、char_traits<char> 我也得学会用"——不必。它们存在的目的正是"让你不用管它们"。看不懂那些参数是正常的。
自测三问(点开对答案) 1. `template ` 之后,`Array<>`、`Array`、`Array` 分别是什么?——**① `Array<>` = `Array`(全默认);② `Array` = `Array`(只覆盖类型);③ `Array` 全指定。** 注意**不能跳过中间的参数**。 2. 为什么标准库要给"第二个、第三个模板参数"都设默认值?——**为了"能长大而不弄坏老代码"**:老代码写的是 `vector`,当年可能只有 `T` 一个参数;后来加了分配器参数,**因为给了默认值,老代码一行不用改**。这是"向后兼容"的机械原理。 3. 非类型模板参数最重要的用途是什么?——**当"编译期常量"用**,最典型就是**数组长度**:`T data[size];`。**因为 `size` 是编译期常量,才能写在数组维度上**——这是普通变量做不到的。

相关笔记