第一章:模板基础回顾——函数模板与类模板的基本语法、模板实例化机制、模板参数推导规则

各位同学,咱们开始上课。

模板这东西,说白了就是C++里的「代码生成器」。你写一套逻辑,编译器帮你生成多份不同版本的代码。我刚开始学模板时,觉得它就是个高级宏替换。后来踩了不少坑才明白——模板远不止这么简单。

这一章,我们快速回顾模板的基础知识。如果你已经用过模板,可以当复习;如果你是新手,请务必把每个例子亲手敲一遍。

1.1 函数模板:让函数「自动适配」类型

先看一个最经典的场景:写一个求两个数最大值的函数。

// 传统写法:每种类型写一遍
int max_int(int a, int b) { return a > b ? a : b; }
double max_double(double a, double b) { return a > b ? a : b; }

// 模板写法:一次搞定
template <typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

看到区别了吗?模板把类型参数化了。typename T 就是占位符,编译器看到你调用 max(3, 5) 时,自动把 T 替换成 int。

个人习惯:我一般用 typename 而不是 class 来声明模板参数。虽然两者等价,但 typename 语义更清晰——它可以是任何类型,不限于类。

函数模板可以有多参数:

template <typename T1, typename T2>
void print_pair(T1 a, T2 b) {
    std::cout << a << ", " << b << std::endl;
}

调用时,编译器会分别推导 T1 和 T2 的类型。

1.2 类模板:让类也「模板化」

类模板和函数模板思路一样,但语法上多了个尖括号在类名后面。

template <typename T>
class Box {
public:
    Box(T value) : value_(value) {}
    T get() const { return value_; }
private:
    T value_;
};

// 使用
Box<int> int_box(42);
Box<std::string> str_box("hello");

注意:类模板的成员函数定义,如果写在类外,需要重复 template 声明:

template <typename T>
T Box<T>::get() const {
    return value_;
}

嗯,这里有个坑——我早期写类模板时,经常忘记在类外定义时加上 template <typename T>,结果编译器报一堆看不懂的错误。后来养成习惯:看到类模板的成员函数定义,第一反应就是检查 template 声明。

1.3 模板实例化机制:编译器在背后做了什么?

模板不是真正的「代码」,它是一份「蓝图」。编译器看到模板定义时,不会生成任何代码。只有当你真正使用它时,编译器才会实例化。

举个例子:

template <typename T>
T add(T a, T b) { return a + b; }

// 下面这行代码触发 int 版本的实例化
int result = add(3, 4);

// 下面这行代码触发 double 版本的实例化
double result2 = add(3.14, 2.86);

编译器会生成两份独立的函数:int add(int, int) 和 double add(double, double)。它们互不干扰。

关键点:模板实例化发生在编译期,不是运行期。所以模板错误会在编译时暴露,而不是运行时崩溃。这也是模板元编程能玩出花样的基础。

我曾经在项目中遇到一个问题:模板函数定义放在 .cpp 文件里,结果链接时报错「未定义引用」。原因就是——模板实例化需要看到完整定义。所以模板代码通常都放在头文件中。

1.4 模板参数推导规则:编译器如何猜你的心思?

函数模板调用时,编译器会根据实参推导模板参数。规则其实不复杂,但有几个细节容易翻车。

规则一:从实参类型推导

template <typename T>
T max(T a, T b);

max(1, 2);       // T 推导为 int
max(1.0, 2.0);   // T 推导为 double

规则二:不同类型实参怎么办?

max(1, 2.0);  // 编译错误!T 无法同时是 int 和 double

解决办法有两个:要么显式指定模板参数 max<double>(1, 2.0),要么让模板接受两个不同类型参数。

规则三:模板参数可以部分指定

template <typename T1, typename T2>
void func(T1 a, T2 b);

func(1, 2.0);           // T1=int, T2=double
func<double>(1, 2.0);   // T1=double, T2=double(显式指定第一个)

显式指定的参数从左往右匹配,剩下的由编译器推导。

避坑指南:我曾经写过一个模板函数,参数类型是 const T&,结果传进去一个临时对象,编译器推导出 T 为 int 而不是 const int&。原因是——引用折叠规则和推导规则叠加后,结果可能和你想的不一样。遇到这类问题,建议用 static_assert 或 typeid 打印类型确认。

1.5 知识体系总览

下面这张图,帮你把本章的核心脉络串起来:

模板基础回顾 函数模板 类模板 实例化机制 template <typename T> T max(T, T) 多参数:T1, T2 class Box<T> { T get(); } 类外定义需重复 template 编译期生成具体代码 定义需在头文件中 模板参数推导规则 从实参类型推导 不同类型需显式指定 可部分显式指定

1.6 本章小结

模板基础就这些内容。函数模板和类模板的语法不难,难的是理解「模板不是代码,而是生成代码的规则」。实例化发生在编译期,所以模板代码必须可见。参数推导规则记住三条:从实参推、不同类型要显式、可以部分指定。

下一章我们会深入模板的特化与偏特化——那才是模板真正开始展现威力的地方。到时候我会分享一个我实际项目中用偏特化解耦接口的案例,很有意思。


可可精品资料库