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

模板参数也能有默认值——template <typename T = int> class Box { ... }; 之后,Box<> 就等于 Box<int>。三条铁律必须记牢:① 默认值必须"从右往左连续给"(template <class T = int, class U> 是错的);② 同一个作用域里只能给一次默认值;③ 类模板能用 Box<> 省掉全部实参,但那个 <> 不能省(写 Box b; 依然错)。还有一条容易搞混的:函数模板也能有默认模板参数(C++11 起),但它"不能用来推导",所以实际很少用——本节的主角是类模板。

📝 本节和前面几节的关系

12.2.2 的 Pair<T1, T2> 要写全两个模板实参才能用。本节给它(以及所有多参数类模板)装上"默认值",让"最常用的那一种组合"可以少写几个字。 它和 6.2.3 默认参数是一对镜像:那里默认的是"函数的参数值",这里默认的是"模板的参数类型"。两者都遵守同一条规矩——默认值必须从右边一路给过来,中间不能断。

一、语法:模板参数也可以"带一个默认值"

template <typename T = int>          // ★ 不写实参时,T 默认是 int
class Box {
private:
    T value;
public:
    Box(T v = T()) : value(v) { }
    T get() const { return value; }
    void show() const { cout << value << endl; }
};

三种用法:

Box<>       a;          // ★ 空尖括号:全部用默认值 → 等于 Box<int>
Box<double> b(1.5);     // ★ 只写第一个:T = double
Box<char>   c('x');     // ★ 全指定

Box<> 这个"空尖括号"是本节的标志性写法——它表示"我就是要用那个模板,只是实参全走默认"。

二、三条铁律

铁律 1:默认值必须从右往左连续给

template <typename T, typename U = int>              // ✓ 合法
class A { };

template <typename T = int, typename U = int>        // ✓ 合法(都有默认)
class B { };

template <typename T = int, typename U>              // ✗ 错!
class C { };

报错(GCC):

error: no default argument for 'U'

为什么? 因为调用时实参是从左往右填的:

C<int> c;      // T 给了 int,U 给谁?—— 你没写,而 U 也没默认 → 无解

规则一句话:

一旦某个模板参数有了默认值,它右边的所有参数都必须有默认值。 和函数的默认参数,是同一条规矩(6.2.3)。

铁律 2:同一个作用域里,默认值只能给一次

template <typename T = int> class Box;        // 第一次声明,给了默认值

template <typename T = int> class Box { ... };// ✗ 错:重复给默认值
template <> class Box<int> { ... };           // ✓ 特化是另一回事(12.3.1)

正确做法:只在一处写默认值(通常写在第一次声明或定义里,看代码怎么组织)。

报错:error: redefinition of default argument。

铁律 3:类模板可以用 Box<>,但 <> 本身不能省

Box<>      a;      // ✓ 全部走默认
Box        b;      // ✗ 错:`class template argument required`

这一点和函数模板感觉完全不同:函数模板调用时可以不写 <...>(编译器从参数类型推)——类模板没有"参数"可推,所以即使所有模板参数都有默认值,也必须写出那对尖括号。

一句话:默认值能替你省掉"里面的东西",但省不掉"那对尖括号"。

三、函数模板的默认模板参数:能写,但很少用

从 C++11 起,函数模板也允许给默认模板参数:

template <typename T = int>
T f(T x = T()) { return x; }

但有个关键限制:默认模板参数"不能用来推导 T"。

f(3);        // ✓ T 从参数推出来 = int(默认值没用上)
f<int>();    // ✓ 显式指定 T = int,x 用函数默认值 0
f();         // ✗ 错!—— 靠"默认模板参数"推不出来

为什么 f() 是错的:C++ 规定"函数模板的类型参数不能从默认模板参数推导"——只有你显式写了 <...>,默认值才有机会生效。

实际结论:函数模板的默认模板参数几乎没用(因为函数调用通常不写 <>)。这个特性主要是给类模板用的——本节后面的例子、以及标准库,都是类模板在用。

四、别和"函数默认参数"搞混

两者长得像、名字像,但默认的是完全不同的东西:

函数默认参数(6.2.3) 默认模板参数(本节)
给谁默认值 函数的变量参数(如 int n) 模板参数(类型如 T,或值如 int N)
写法 void f(int n = 10); template <typename T = int>
什么时候生效 调用函数时省略实参 写模板实参时省略(Box<>)
"从右往左" 同一条规矩 同一条规矩
能不能省掉括号 ——(本来就没有) 不能,<> 必须写

两者可以同时出现在一个函数上:

template <typename T = int>
void f(T a = T(), int n = 10) { ... }      // 前面是"默认模板参数",后面是"函数默认参数"

五、动手 Demo:Box 的三种写法

#include <iostream>
#include <string>
using namespace std;

template <typename T = int, typename U = double>
class Box {
private:
    T a;
    U b;
public:
    Box(T x = T(), U y = U()) : a(x), b(y) { }
    void show() const {
        cout << "(" << a << ", " << b << ")" << endl;
    }
};

int main() {
    Box<>               d;              // ★ 全默认:T = int, U = double
    Box<string>         s("hi");        // ★ 只给第一个:T = string, U = double
    Box<char, int>      c('A', 65);     // ★ 全指定
    Box<string, string> ss("左", "右");

    d.show();
    s.show();
    c.show();
    ss.show();
    return 0;
}

看到什么算成功:

(0, 0)
(hi, 0)
(A, 65)
(左, 右)

读结果:

写法 实际得到的类 依据
Box<> d; Box<int, double> 两个默认值都生效
Box<string> s("hi"); Box<string, double> 只覆盖第一个,第二个仍是默认的 double → 所以 b 打印出 0(double 的零值)
Box<char, int> c('A', 65); 全指定 ——
Box<string, string> ss("左","右"); 全指定 ——

30 秒实验(把两条铁律各撞一次):

  1. 把类模板头改成 template <typename T = int, typename U>(删掉 U 的默认值) → 编译错:no default argument for 'U'。
  2. 改回 U = double,再把 main 里的 Box<> d; 改成 Box d;(去掉尖括号) → 编译错:class template argument required for 'class Box'。
  3. 两条报错都亲手见一次,以后一眼能认出来。

六、常见误区

  1. "有默认值就能写 Box b; 了"——不能。<> 必须写(Box<> b;)。默认值省的是"里面的实参",不是"那对尖括号"。
  2. "template <class T = int, class U> 也行吧"——不行。默认值必须从右往左连续:一旦左边有了默认值,右边的全部都要有。
  3. "函数模板也能靠默认模板参数省略类型"——不能。f() 这种写法是错的:默认模板参数不能用于推导。函数模板的类型通常靠函数参数推出来。
  4. "默认模板参数和函数默认参数是一回事"——不是。前者默认的是"类型"(或非类型的值),后者默认的是"变量的值"。
  5. "Box<> 和 Box 都行,前面那个只是风格"——不是风格,前者合法、后者报错。
  6. "默认值可以随便重复写"——同一作用域只能给一次,重复给报 redefinition of default argument。
自测三问(点开对答案) 1. `template class C { };` 错在哪里?——**默认值没"从右往左连续给"**:`T` 有默认值,但它右边的 `U` 没有。报 `no default argument for 'U'`。改成 `template ` 或两个都给默认值。 2. 类模板的模板参数全都有默认值时,能不能写 `Box b;`?——**不能**。**必须写 `Box<> b;`**——**那对尖括号省不掉**。这是类模板和函数模板的一个重要差别(函数模板靠参数推导,可以不写 `<>`)。 3. 函数模板的默认模板参数为什么"几乎没用"?——因为**默认模板参数不能用于推导**:`f()` 会报错,必须 `f()` 才用得上。而函数调用时本来就不写 `<>`,**所以这个特性主要是给类模板用的**。

相关笔记