模板与编译期

自动关联目录:模板与编译期

模板不是"泛型",是"编译期生成代码"。理解这一点,就能解释它的全部特性:错误信息难读、头文件必须可见、改一行重编一大片、以及"零成本抽象"为什么成立。

一句话定位:模板的代价发生在编译期(时间、体积、可读性),收益发生在运行期(没有间接跳转、可内联)。

实例化

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
认为模板一定比虚函数快只有静态绑定才快,且要能内联