外观
Stage07|Modern C++ Foundation
Lesson089|Template、泛型代码与编译期实例化
Tags: #C++17 #Template #Generic #InstantiationDifficulty: ⭐⭐⭐⭐⭐
一、问题场景:模板声明存在却链接失败
cpp
// convert.hpp
template<class T>
T convert(const std::string& text);如果定义只放在 convert.cpp,使用它的翻译单元可能无法实例化目标类型,最终出现链接错误。模板不仅是“类型占位符”,还改变了代码何时生成。
二、本课目标
- 理解函数模板、类模板和实例化。
- 理解模板为什么通常定义在头文件。
- 认识类型推导、特化和
if constexpr。 - 读懂模板错误的第一条有效约束。
- 控制模板带来的耦合和编译成本。
三、回到 TaskSystem:哪些变化值得泛型化
任务系统可以把“执行一个可调用对象”泛型化,但不应为了模板而让错误信息和编译时间失控。先实现一个明确的 std::function<void()> 版本,再观察复制、类型擦除和性能瓶颈,最后决定是否用模板保留具体类型。
四、从函数模板开始
cpp
template<class T>
const T& maxValue(const T& a, const T& b) {
return a < b ? b : a;
}调用 maxValue(1, 2) 时,编译器推导 T=int 并生成相应实例。调用类型必须支持函数体中使用的操作,这里是 <。
五、类模板
cpp
template<class T, std::size_t Capacity>
class FixedBuffer {
public:
T& operator[](std::size_t index) { return data_[index]; }
constexpr std::size_t size() const { return Capacity; }
private:
std::array<T, Capacity> data_{};
};类型参数和非类型参数共同构成不同类型:FixedBuffer<int, 16> 与 FixedBuffer<int, 32> 互不相同。
六、实例化与头文件
编译器实例化时通常必须看到完整定义,所以模板实现常放 .hpp。另一种方式是显式实例化:
cpp
// convert.cpp
template int convert<int>(const std::string&);这能控制代码生成和编译依赖,但只支持明确列出的类型。库接口需在灵活性和二进制边界之间选择。
七、if constexpr
cpp
template<class T>
void printValue(const T& value) {
if constexpr (std::is_pointer_v<T>) {
if (value) std::cout << *value;
} else {
std::cout << value;
}
}未选中的分支在实例化时被丢弃,适合表达编译期类型分支。普通 if 的两个分支都必须在类型层面有效。
八、特化与重载边界
函数行为差异通常优先考虑重载;类模板在确实需要不同结构时使用偏特化或全特化。过度特化会让规则难以预测,调用方也难判断选中了哪个实现。
九、可编译验证:观察实例
cpp
#include <iostream>
#include <string>
#include <type_traits>
template<class T>
T twice(T value) { return value + value; }
int main() {
std::cout << twice(21) << '\n';
std::cout << twice(std::string("ab")) << '\n';
}分别实例化 int 和 std::string。再传入一个没有 operator+ 的类型,阅读诊断时从“requested here/实例化于此”回到自己的调用点,不要从标准库最深层开始看。
十、TaskSystem 的泛型边界
模板适合表达“对任意满足约束的任务执行”,不适合隐藏业务状态机。C++17 没有 Concepts,可以通过 static_assert、清晰的命名和小型 trait 给出可读约束;若错误信息已经比业务逻辑难懂,应退回普通接口或类型擦除。
十一、编译成本和代码膨胀
模板定义进入每个使用它的翻译单元,会增加解析和实例化成本。不同类型还可能生成多份机器码。优化措施包括减少重型头文件暴露、显式实例化常用类型、使用非模板内部实现,以及先测量构建时间和二进制体积。
十二、练习与答案
1. 模板是否一定在编译期执行?
模板在编译期生成代码,但生成的函数通常仍在运行时执行。只有 constexpr 且满足常量表达式条件时才可能在编译期求值。
2. 为什么模板错误很长?
一次不满足可能沿嵌套实例化链展开。应先找到自己的调用点和第一条类型要求失败,而不是逐行修标准库内部文本。
3. 何时不该用模板?
类型集合固定、需要稳定 ABI、实现应隐藏或编译成本过高时,普通多态、类型擦除或显式接口可能更合适。
十三、本课总结
模板把代码生成推迟到具体类型出现时。它提供零额外抽象能力,也把类型约束、构建依赖和错误复杂度带到编译期。
