Skip to content

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,使用它的翻译单元可能无法实例化目标类型,最终出现链接错误。模板不仅是“类型占位符”,还改变了代码何时生成。

二、本课目标 ​

  1. 理解函数模板、类模板和实例化。
  2. 理解模板为什么通常定义在头文件。
  3. 认识类型推导、特化和 if constexpr。
  4. 读懂模板错误的第一条有效约束。
  5. 控制模板带来的耦合和编译成本。

三、回到 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、实现应隐藏或编译成本过高时,普通多态、类型擦除或显式接口可能更合适。

十三、本课总结 ​

模板把代码生成推迟到具体类型出现时。它提供零额外抽象能力,也把类型约束、构建依赖和错误复杂度带到编译期。