本节摘要:模板不是运行期的"泛型对象",而是编译期的代码生成器——函数模板按调用的类型各生成一份代码,类模板按实例化参数各生成一个类。这一机制同时解释了模板为何零运行期开销、错误信息为何冗长、定义为何常驻头文件。本节讲透函数模板与类模板的实例化、特化与偏特化,以及 C++20 概念如何驯服报错。
阅读完本节,你应当能够:
template <typename T> T max_of(T a, T b) { return a < b ? b : a; } int main() { max_of(3, 7); // 编译器生成 int 版本 max_of(2.5, 1.5); // 再生成 double 版本 max_of(3, 2.5); // 编译错误:T 既要 int 又要 double 推导失败 }
max_of 不是"能处理任何类型的函数",而是"生产函数的图纸"。每遇到一种新类型组合,编译器按图纸现场生成一份专用代码——这个过程叫实例化,发生在编译期。所以运行期没有任何类型分派的开销(对比 2.3 节的虚表跳转:模板是编译期多态,虚函数是运行期多态),代价则转移到编译时间与代码体积(每种类型一份代码)。推导失败的报错之所以冗长,正是因为它发生在实例化深处——编译器要把整条生成链吐给你看。
类模板同理,std::vector<int> 与 std::vector<std::string> 是两个完全独立的类:
template <typename T> class Stack { public: void push(const T& v) { data_.push_back(v); } void push(T&& v) { data_.push_back(std::move(v)); } // 左右值分治 T pop() { T top = std::move(data_.back()); data_.pop_back(); return top; } bool empty() const { return data_.empty(); } private: std::vector<T> data_; // 组合现成容器 零法则受益者 };
类模板成员函数也是"用到才实例化"——Stack<Foo> 只要不调 push,Foo 没有拷贝构造也能编译通过。这份惰性给了模板惊人的容纳力。
通用图纸之外,个别类型需要专属实现,用特化:
#include <cstring> #include <string> template <typename T> bool equals(const T& a, const T& b) { return a == b; } // 对 const char* 全特化:比较内容而非地址 template <> bool equals<const char*>(const char* const& a, const char* const& b) { return std::strcmp(a, b) == 0; }
类模板还支持偏特化(对模板参数的一部分模式特化),标准库经典案例是 std::vector<bool>——按位压缩的特化版本,sizeof 语义、代理引用等行为与普通 vector 不同,是"特化能走多远"的极端展示,也是"特化要谨慎"的反面教材(行为差异大到让使用者困惑)。特化的纪律:只改实现,不改语义——特化版本应当仍满足使用者对主模板的合理预期。
非类型模板参数让"值"也参与编译期定制:
template <typename T, std::size_t N> class FixedBuf { T data_[N]; // 尺寸编译期定 栈上分配 零堆开销 public: constexpr std::size_t size() const { return N; } }; FixedBuf<double, 64> window; // 64 个 double 全在栈上
std::array 正是这个思路的标准产物——把 1.4 节裸数组的速度与容器的接口合体。
模板体系配套的现代化糖,逐个过一遍:
auto 与 decltype:让编译器当类型书记员。auto it = m.find(key); 省去拼写 std::map<K,V>::iterator;遍历一律 for (const auto& item : container),const 引用既不拷贝又防误改。
可变参数模板:接受任意个参数,配合完美转发构成 emplace 系列的地基:
#include <utility> template <typename... Args> void log_all(Args&&... args) { (..., print_one(std::forward<Args>(args))); // C++17 折叠表达式 }
sizeof...(args) 给出参数个数。std::make_unique、vector::emplace_back 内部正是这套机制——3.4 节的完美转发在这里找到大规模应用。
编译期计算:constexpr 把计算推到编译期:
constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); } constexpr int k_table_size = factorial(8); // 编译期算好 运行期零成本
模板元编程(在类型上做条件、递归的编程范式)是这一思想的极致形态,std::conditional、std::enable_if 都是它的产物。适用边界要清醒:能用普通 constexpr 函数解决的,别动元编程;模板元编程留给"必须依赖类型信息做决策"的场合(序列化库、表达式模板)。
传统模板的痛点是约束隐式——T 要支持 <,写模板的人知道,用的人只在一屏报错里猜。C++20 的概念把要求写成签名的一部分:
#include <concepts> template <typename T> concept Numeric = std::integral<T> || std::floating_point<T>; template <Numeric T> // 约束显式化 T max_of(T a, T b) { return a < b ? b : a; } max_of(3, 7); // 通过 max_of("a", "b"); // 编译错误:直接说明不满足 Numeric 概念
报错从"生成链崩溃日志"变成"一句不满足约束",泛型代码的可用性跨代提升。即便项目停留在 C++17,static_assert 加类型特征也能达到八成效果:
template <typename T> void serialize(const T& v) { static_assert(std::is_trivially_copyable_v<T>, "serialize 要求可平凡拷贝的类型"); // 错误信息人话化 /* ... */ }
⚠️ 常见坑三条:模板定义放源文件导致链接错误(模板要见到完整定义才能生成代码,惯例放头文件);拿两个不相关模板实例互相比大小(
vector<int>与vector<double>是不同类型,不能隐式互转);在模板里对 T 调用不存在的成员,报错出现在使用者一侧而非模板一侧——用概念或 static_assert 把错误前移。
下一节把异常扔进这套体系:栈展开如何逐一析构路过对象,你的 RAII 外套是否真的滴水不漏。