文章目录
- 引言
- 一、第一个函数模板
-
- 1.1 语法拆解
- 1.2 调用方式
- 1.3 模板不是万能的——类型必须支持模板中用到的操作
- 二、编译器如何生成代码:模板实例化
-
- 2.1 两阶段编译
- 2.2 隐式实例化
- 2.3 显式实例化
- 三、模板参数推导规则
-
- 3.1 基本的推导
- 3.2 函数参数中的引用和 const
- 四、模板与重载:谁先被匹配
- 五、非类型模板参数
- 六、函数模板的常用实战模式
-
- 6.1 引用封装——免除手动取地址
- 6.2 比较器封装
- 6.3 通用的输出函数
- 七、模板与头文件:为什么模板不能分开编译
- 总结
本系列为《C++深度修炼:基础、STL源码与多线程实战》第25篇 前置条件:理解函数调用机制(C语言),了解 auto(第13篇),了解引用(第9篇)
引言
C 语言中,要写"对任意类型都适用"的函数,你只有两条路——宏,或者 void*:
// 方案一:宏——没有类型检查,调试时看到的不是原始代码
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// 方案二:void* ——丧失类型安全,调用方必须强转
int max_int(void *a, void *b) {
return *(int*)a > *(int*)b ? *(int*)a : *(int*)b;
}
宏的致命伤:MAX(++x, y) 会把 ++x 展开两次。void* 的致命伤:编译器不知道你在比较 int 还是 double,错误要等到运行时才暴露。
C++ 的函数模板同时解决了这两个问题:像宏一样对任意类型适用,像函数一样有完整的类型检查。
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}
这就是本章的起点——写出第一个函数模板,理解编译器"按需生成代码"的机制,并避开模板编程中最常见的坑。
一、第一个函数模板
1.1 语法拆解
template <typename T> // ← "T 是一个类型参数,调用时由编译器推导"
T max(T a, T b) { // ← 用 T 就像用 int——可以写参数、返回值、局部变量
return a > b ? a : b;
}
typename 也可以用 class 替代——两者在模板参数声明中完全等价:
template <class T> // 和上面的写法意义完全相同
T max(T a, T b) { return a > b ? a : b; }
1.2 调用方式
#include <iostream>
#include <string>
template <typename T>
T max(T a, T b) { return a > b ? a : b; }
int main() {
// 隐式推导:编译器根据参数类型自动推导 T
std::cout << max(3, 5) << '\\n'; // T = int
std::cout << max(3.14, 2.71) << '\\n'; // T = double
std::string s1 = "apple", s2 = "banana";
std::cout << max(s1, s2) << '\\n'; // T = std::string
// 显式指定:当推导歧义或需要强制类型时
std::cout << max<double>(3, 5.5) << '\\n'; // T = double——3 被隐式转换为 3.0
}
编译器不会生成一个"泛型"的 max 函数。它会在每次调用时,根据 T 的具体类型实例化出一份独立的机器码——相当于编译器帮你把 max<int>、max<double>、max<std::string> 各写了一遍。
1.3 模板不是万能的——类型必须支持模板中用到的操作
struct Point { int x, y; };
int main() {
Point p1{1, 2}, p2{3, 4};
// auto p = max(p1, p2); // ❌ 编译错误:Point 没有 operator>
}
模板代码中用到了 a > b——所以 T 必须支持 operator>。模板的类型约束是隐式的——编译器不会提前检查,而是在实例化时报告"T 没有 operator>"。这是模板报错冗长的根本原因(第27篇会专门讲如何读懂和化解)。
二、编译器如何生成代码:模板实例化
2.1 两阶段编译
模板代码经历两个阶段:
template <typename T>
void foo(T x) {
std::cout << x.member; // 第一阶段不报错——因为还不知道 T 有没有 member
int a = "hello"; // ❌ 第一阶段就报错:与 T 无关,肯定不合法
}
2.2 隐式实例化
只有被调用的模板函数才会生成代码:
template <typename T>
T add(T a, T b) { return a + b; }
int main() {
auto x = add(1, 2); // 生成 add<int>
auto y = add(1.5, 2.5); // 生成 add<double>
// add<std::string> 没有被生成——因为没有调用 add(string, string)
}
这就意味着:模板可以放在头文件中而不会导致"重复定义"链接错误——编译器在每一处调用生成一份相同的实例化,链接器合并它们(或者编译器用 inline 语义处理)。
2.3 显式实例化
如果你想提前生成某份实例化(例如为了减少编译时间,或把实例化集中在一个 .cpp 中):
// 显式实例化声明(告诉编译器:别在这里生成,有人在别处生成了)
extern template int max<int>(int, int);
// 显式实例化定义(告诉编译器:在这里生成这份实例化)
template int max<int>(int, int);
三、模板参数推导规则
3.1 基本的推导
template <typename T>
T max(T a, T b) { return a > b ? a : b; }
max(1, 2); // T = int
max(1.0, 2.0); // T = double
// max(1, 2.0); // ❌ 歧义:T 是 int 还是 double?
当两个参数推导出不同的 T 时,编译直接失败——模板参数推导不会做隐式类型转换(只有少数例外:左值到右值、数组到指针、函数到函数指针、cv 限定符调整)。
解决方式有两种:
// 方式一:调用时显式指定
max<double>(1, 2.0); // T = double,1 被转换为 1.0
// 方式二:用两个模板参数
template <typename T1, typename T2>
auto max(T1 a, T2 b) { return a > b ? a : b; }
max(1, 2.0); // ✅ T1 = int, T2 = double,返回类型由 ?: 规则推导
3.2 函数参数中的引用和 const
template <typename T>
void f(T param); // param 是值类型——顶层 const 被忽略
template <typename T>
void g(T ¶m); // param 是引用——const 被保留
int x = 42;
const int cx = 42;
const int &rx = x;
f(x); // T = int
f(cx); // T = int(顶层 const 被忽略)
f(rx); // T = int(引用性被忽略——rx 是 const int 的引用,T 推导为 int)
g(x); // T = int
g(cx); // T = const int(const 被保留)
g(rx); // T = const int
这是模板参数推导中最容易出错的地方,尤其当你把模板函数和 std::forward 结合使用时(第31篇会详细展开)。
四、模板与重载:谁先被匹配
// 普通函数
int max(int a, int b) { return a > b ? a : b; }
// 模板函数
template <typename T>
T max(T a, T b) { return a > b ? a : b; }
int main() {
max(1, 2); // 调用普通函数——当普通函数和模板都匹配时,优先选普通函数
max<>(1, 2); // 调用模板——<> 告诉编译器"只考虑模板"
max(1.0, 2.0); // 调用模板——普通函数的参数类型不匹配,只有模板可用
}
重载决议的顺序:
五、非类型模板参数
除了类型,模板参数还可以是编译期常量:
// size 不是类型——它是编译期已知的整数值
template <typename T, size_t N>
size_t array_size(T (&arr)[N]) {
return N;
}
int main() {
int arr[10];
double darr[5];
std::cout << array_size(arr) << '\\n'; // 10
std::cout << array_size(darr) << '\\n'; // 5
// T 分别推导为 int 和 double,N 分别推导为 10 和 5
}
非类型模板参数可以是:整型常量、枚举、指针(指向全局/静态对象)、引用、nullptr_t、C++20 起还支持浮点和类类型(受限)。
// 编译期计算阶乘(模板元编程的经典入门例子)
template <unsigned N>
struct Factorial {
static const unsigned value = N * Factorial<N – 1>::value;
};
template <>
struct Factorial<0> {
static const unsigned value = 1;
};
int main() {
std::cout << Factorial<5>::value << '\\n'; // 120——编译期计算完成
}
六、函数模板的常用实战模式
6.1 引用封装——免除手动取地址
template <typename T>
void zero_init(T &obj) {
std::memset(&obj, 0, sizeof(T));
}
// 对 int、double、struct 都适用——不需要 void*
6.2 比较器封装
template <typename T, typename Compare>
const T& min(const T &a, const T &b, Compare comp) {
return comp(a, b) ? a : b;
}
// 第三个参数可以是函数指针、lambda、函数对象——编译器会为每种组合生成独立实例化
6.3 通用的输出函数
template <typename Container>
void print(const Container &c, const char *sep = " ") {
for (const auto &x : c) {
std::cout << x << sep;
}
std::cout << '\\n';
}
七、模板与头文件:为什么模板不能分开编译
这是初学者最困惑的问题之一:
// ===== max.h =====
template <typename T>
T max(T a, T b); // 声明
// ===== max.cpp =====
template <typename T>
T max(T a, T b) { // 定义
return a > b ? a : b;
}
// ===== main.cpp =====
#include "max.h"
int main() {
max(1, 2); // ❌ 链接错误:找不到 max<int> 的实现!
}
原因: 编译器编译 max.cpp 时,不知道你会调用 max<int>,所以没有生成 max<int> 的实例化。编译 main.cpp 时,看到声明但没有定义——也生成不了。最终链接器找不到 max<int> 的机器码。
解决方案——把实现放在头文件中:
// ===== max.hpp =====
#pragma once
template <typename T>
T max(T a, T b) { // 定义直接放在头文件中
return a > b ? a : b;
}
或者用 #include 技巧(不太推荐但有时需要):
// ===== max.h =====
template <typename T> T max(T a, T b);
#include "max_impl.hpp" // 在头文件末尾包含实现
这也是为什么 STL 的头文件(<vector>、<algorithm>)里全是模板实现——没有对应的 .cpp 文件。
总结
函数模板是 C++ 泛型编程的入口——它让你写一份代码适用所有类型,同时享有完整的类型安全和编译期优化机会:
下一篇文章我们将进入类模板的世界——如何用 template 构建泛型的容器、RAII 包装器,以及 C++17 的 CTAD 如何让我们写更少的 <>。
动手练习:



