3.5 模板的编译期展开


3.5 模板的编译期展开

本节摘要:模板不是运行期的"泛型对象",而是编译期的代码生成器——函数模板按调用的类型各生成一份代码,类模板按实例化参数各生成一个类。这一机制同时解释了模板为何零运行期开销、错误信息为何冗长、定义为何常驻头文件。本节讲透函数模板与类模板的实例化、特化与偏特化,以及 C++20 概念如何驯服报错。

你将学会

阅读完本节,你应当能够:

  1. 写出函数模板与类模板并说明实例化发生在何时;
  2. 用特化定制个别类型的实现;
  3. 用 auto 与可变参数模板简化接口;
  4. 解释模板元编程的编译期计算思想与适用边界。

一、先看模板"变出"了什么

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、可变参数与折叠表达式

模板体系配套的现代化糖,逐个过一遍:

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_uniquevector::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::conditionalstd::enable_if 都是它的产物。适用边界要清醒:能用普通 constexpr 函数解决的,别动元编程;模板元编程留给"必须依赖类型信息做决策"的场合(序列化库、表达式模板)。

四、概念:给模板上锁(C++20)

传统模板的痛点是约束隐式——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 把错误前移。

本节要点回顾

  • 模板是代码生成器:实例化发生在编译期,零运行期分派开销,代价是编译时间与代码体积。
  • 编译期多态对运行期多态:模板按类型静态展开,虚函数经虚表动态分发;前者快、后者灵活,选择即取舍。
  • 特化只改实现不改语义;偏特化与 vector 提醒特化的边界。
  • 配套四件套:auto 推导、可变参数加折叠、constexpr 编译期计算、概念约束——层层递进地把泛型写得又快又好懂。
  • 定义进头文件、错误前移:模板工程化的两条铁律。

下一节把异常扔进这套体系:栈展开如何逐一析构路过对象,你的 RAII 外套是否真的滴水不漏。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U