模板与编译期
自动关联目录:模板与编译期
模板不是"泛型",是"编译期生成代码"。理解这一点,就能解释它的全部特性:错误信息难读、头文件必须可见、改一行重编一大片、以及"零成本抽象"为什么成立。
一句话定位:模板的代价发生在编译期(时间、体积、可读性),收益发生在运行期(没有间接跳转、可内联)。
实例化
template<typename T>
T Max(T a, T b) { return a > b ? a : b; }
// 用到才实例化
int x = Max(1, 2); // 生成 Max<int>
double y = Max(1.0, 2.0); // 生成 Max<double>| 要点 | 说明 |
|---|---|
| 隐式实例化 | 用到才生成 |
| 显式实例化 | template int Max<int>(int, int);——可放进 .cpp,减少重编 |
| 显式特化 | template<> const char* Max<const char*>(...) |
| 偏特化 | 只对类模板有效,函数模板不支持偏特化 |
模板定义必须放在头文件里(除非显式实例化)——这是"改头文件全量重编"的根源之一。
编译期的三个代价
| 代价 | 表现 | 缓解 |
|---|---|---|
| 编译时间 | 模板重的工程编译以分钟计 | 显式实例化、减少嵌套、extern template |
| 代码膨胀 | 每个类型一份代码 | 把类型无关的部分抽到非模板基类 |
| 错误信息 | 几百行模板栈 | static_assert 加约束 + Concepts(C++20) |
// 减少膨胀的经典手法:类型无关部分下沉
class VectorBase { /* 非模板实现,存 void* 或 char* */ };
template<typename T> class Vector : private VectorBase { /* 只做类型转换 */ };SFINAE 与 Concepts
SFINAE(Substitution Failure Is Not An Error):替换失败不算错误,只是把这个候选从重载集里去掉。
// 传统:SFINAE
template<typename T>
auto Print(const T& v) -> decltype(v.Print(), void()) { v.Print(); }
// C++17:if constexpr,可读性最好
template<typename T>
void Print(const T& v)
{
if constexpr (requires { v.Print(); }) { v.Print(); }
else { /* 其它路径 */ }
}
// C++20:Concepts,错误信息最清楚
template<typename T>
concept Printable = requires(T v) { v.Print(); };
template<Printable T>
void Print(const T& v) { v.Print(); }能用 Concepts 就用 Concepts——它把"几百行模板栈"变成"不满足约束 Printable"一行。
CRTP:静态多态
template<typename Derived>
class Base
{
public:
void Interface() { static_cast<Derived*>(this)->Impl(); }
};
class Foo : public Base<Foo>
{
public:
void Impl() { /* ... */ }
};| 虚函数 | CRTP | |
|---|---|---|
| 决议时机 | 运行期(vptr 查表) | 编译期(静态绑定) |
| 开销 | 一次间接跳转 + vptr 空间 | 无 |
| 能否存异构对象 | 能(基类指针容器) | 不能 |
| 二进制兼容 | 稳定 | 改基类需重编 |
CRTP 省的是一次间接跳转,只在极端热路径(每帧调用百万次)才值得。大多数地方用虚函数更清晰。
变参模板与折叠表达式
template<typename... Args>
void LogAll(Args&&... args)
{
(std::cout << ... << std::forward<Args>(args)) << '\n'; // C++17 折叠表达式
}
// 参数包展开
template<typename... Args>
auto Sum(Args... args) { return (args + ... + 0); }变参模板是"类型安全的可变参数",替代 C 的 va_list。代价是错误信息更难读。
类型萃取与常用工具
| 工具 | 用途 |
|---|---|
std::is_same_v<T, U> | 类型判断 |
std::remove_reference_t<T> / std::decay_t<T> | 去掉引用与 cv |
std::enable_if_t | 条件启用(C++20 前) |
std::void_t | 检测成员是否存在 |
std::is_trivially_copyable_v<T> | 能否 memcpy——性能敏感处常用 |
constexpr / consteval | 编译期计算 |
if constexpr | 编译期分支,未选中的分支不实例化 |
// 编译期判断:trivially copyable 就走 memcpy
if constexpr (std::is_trivially_copyable_v<T>)
{
memcpy(Dst, Src, Count * sizeof(T));
}
else
{
std::copy(Src, Src + Count, Dst);
}编译期计算
constexpr int Fib(int n) { return n <= 1 ? n : Fib(n-1) + Fib(n-2); }
static_assert(Fib(20) == 6765);
// 编译期查表
template<int N> struct Factorial { static constexpr int value = N * Factorial<N-1>::value; };
template<> struct Factorial<0> { static constexpr int value = 1; };constexpr 的收益是把运行期计算变成 0,代价是编译变慢、代码可读性下降。只在真的需要(表是固定的、量很大)时用。
常见坑
| 坑 | 说明 |
|---|---|
模板定义放在 .cpp | 链接错误"未定义的引用" |
| 函数模板偏特化 | 不支持,用重载 |
| 模板递归过深 | 编译爆炸,加终止条件 |
| 过度泛型 | 人人都看不懂,编译又慢 |
| 头文件里实例化重型模板 | 每个 TU 都实例化一遍 |
依赖 SFINAE 而不给 static_assert | 报错无法读懂 |
typename / template 关键字遗漏 | 依赖名称要写 typename T::type |
| 认为模板一定比虚函数快 | 只有静态绑定才快,且要能内联 |
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/3889688a.html
更新于:2026年10月10日