本节摘要:模板不是运行期机制而是开票机制:编译器按每个用到的类型生成一份专属代码(实例化),int 与 string 拿到的是两张不同的票。票面行为取决于类型资质(可拷贝、可移动、noexcept),票的总量决定二进制体积。本节把开票过程做成可观察的实验,并给出控票手段。
本章把镜头从运行期转到编译期。第 3 章讲过:一份函数只有一种形态、一种成本;本章的模板打破这一点——同一份源码,对每个类型开一张专属的票。本节先看清开票机制本身:何时开、开几章、票面长什么样、在哪里合并。
一个模板加两种类型的调用,用typeid加计数把开票过程亮出来:
#include <cstdio> #include <string> #include <typeinfo> template <typename T> T twice(const T& v) { // 模板:源码只有一份 std::printf(" 开票给 %s\n", typeid(T).name()); return v + v; } int main() { std::printf("调用点 1:int\n"); int i = twice(21); std::printf("调用点 2:double\n"); double d = twice(1.5); std::printf("调用点 3:string\n"); std::string s = twice(std::string("ab")); std::printf("调用点 4:再次 int(同一张票)\n"); int i2 = twice(99); std::printf("结果:%d %.1f %s %d\n", i, d, s.c_str(), i2); return 0; }
典型输出(GCC 下类型名是缩写,MSVC 下是完整拼写):
调用点 1:int 开票给 i 调用点 2:double 开票给 d 调用点 3:string 开票给 Ss 或 NSt7__cxx11basic_stringIcSt11char_traitsIcESaIcEEE 调用点 4:再次 int(同一张票) 开票给 i 结果:42 3 ab 198
开票规则四条。其一,按需开票:模板定义本身不产生代码,每个"模板名加具体类型"的首次使用才触发实例化。其二,同类型合并:调用点 4 复用调用点 1 的票,链接器会把多个编译单元里的同一张票合并成一份。其三,票面专属:int 的票里 v + v 是整数加法,string 的票里是字符串拼接——两份不同的机器码。其四,资质检查发生在票内:如果类型不支持 +,报错发生在实例化展开处,而不是模板定义处,这就是模板错误信息又长又深的根源。

类模板的开票逻辑相同,但票面更大,是膨胀审计的主对象。下面这个类模板对三种整型开三张票,用显式实例化把票"钉"在当前编译单元里,方便用符号表观察:
#include <cstdio> template <typename T> struct Box { explicit Box(T v) : v_(v) {} T get() const { return v_; } void describe() const { std::printf("Box 内尺寸 %zu 字节\n", sizeof(T)); } private: T v_; }; // 显式实例化:即使没人用,也在此编译单元生成 int 版本的完整票面 template struct Box<int>; int main() { Box<int> a{1}; Box<long long> b{2}; Box<char> c{'x'}; a.describe(); b.describe(); c.describe(); std::printf("三张票,三个 sizeof:同一逻辑,三份代码\n"); return 0; }
输出:
Box 内尺寸 4 字节 Box 内尺寸 8 字节 Box 内尺寸 1 字节
膨胀的机制看清了:票数 = 使用到的类型组合数,票面 = 模板体的机器码大小。标准库里 vector 每用一个新类型就添一整套票;模板层层嵌套时票数还会相乘。控票三板斧按代价从小到大排:
extern template class Box<int>;,告诉其他编译单元"这张票别开了,链接时找我要"。高频票集中在一处开,其余单元省掉重复开票时间与体积。背景:本章主线的路由模块第一版有三份几乎相同的 route 函数,分别服务整型、字符串、消息体;修一个路由规则要改三处,出过一次漏改事故。
操作:合并成一份 template <typename Key> route(const Key&)。合并后做两件审计:用符号表列出所有实例化票面,确认只有预期的几个类型;把路由表查找、日志这些与 Key 无关的逻辑抽进非模板辅助函数,票面缩小约四成。
结果:源码一份、逻辑单点维护;二进制体积不升反降(三份手抄里本就藏着重复的辅助逻辑);新类型接入只需一个调用点,无需再抄代码。
解读:这次改造的收入是单点维护(源码账),代价是票面账需要管理。模板不是免费的抽象——它把"多份相似代码"从源码里搬进了二进制里。审计视角的结论:搬得值不值,看票面是否可控;可控(类型集合有限、模板体精瘦),泛型就是净赚;失控(类型组合爆炸、模板体臃肿),膨胀就把抽象的收益吃回去。
变式:对接第 3 章——模板票面里 std::forward 转发实参、容器开票时对元素移动与拷贝的自动选择,都是"票面行为由类型资质决定"的例子。票面质量审计因此有个固定条目:你的类型在泛型代码里拿到的票,走的是移动还是拷贝——资质差的类型在所有票里都慢。
💡 关键直觉:模板是"开票机器"不是"运行期魔法"。编译产物里没有"模板"这个实体,只有一张张具体类型的票。读懂报错、控住体积、审好票面行为,三件事都建立在"票"这个模型上。
下一节看编译期怎么查账:type traits 回答类型资质问题,if constexpr 在开票前裁剪分支,enable_if 按资质决定开不开票——泛型代码从此可以"查着账写"。