← 返回模块
3.4.2.1beta 可读 · 未来免费校验通过内容版本 2026-05-27

模板与泛型编程

3.4.2 · 模板与现代 C++ · 编程

国内某私募的 C++ 研究桌周一例会:新入职的研究员上线了 mean_doublemean_floatmean_long_double 三份函数——同一个九行的均值计算被复制了三次,只是浮点精度不同。Senior C++ engineer 的 review 意见只有一句话:「这里要写成模板。」周五新人交回的版本里,三份代码合成了一个 template<typename T> T mean_of(const std::vector<T>&):编译产物更小、bug 更少,并且上季度生产环境里那个 int 溢出 bug 只需要在一个地方修。模板是 C++ 的主要代码复用工具,而 3.4.1 留给你的状态正是 senior engineer 拒掉的那种「单态、复制粘贴」的写法。本课把这件事补齐。

函数模板:最小单元

最小的模板是「函数模板」——以类型为参数的函数家族配方。语法是声明一个类型参数,然后像普通类型一样在签名和函数体中使用它。(本课每一个 Fenced cpp 代码块都是 gate 会按字节核对的精确形式。)

template<typename T>
T min_of(T a, T b) {
    return a < b ? a : b;
}

调用点上,编译器会把每个实参与参数列表比对,​​推导​​出 T 的类型。min_of(1, 2) 推导出 T = int,实例化出 int min_of<int>(int, int)min_of(1.0, 2.0) 推导出 T = double,实例化出 double min_of<double>(double, double)。每一次实例化在编译产物里都是独立的函数;模板定义是配方,实例化是蛋糕。

当实参之间无法统一时,推导失败。min_of(1, 2.0)——一个 int、一个 double——没有任何单一的 T 同时满足两者,编译器会报出类似 no matching function for call to min_of(int, double) 的错误。两种修复方式:在调用点把实参类型统一(min_of(1.0, 2.0)),或者给出显式模板实参(min_of<double>(1, 2.0)),意思是「就用 double,能转就转」。显式模板实参也是你「强制选某个实例化」时的工具——min_of<long>(1, 2) 会实例化出 long 版本,即使 int 本来就能用。

完整的推导算法——顶层 cv 限定符剥离、引用折叠、数组退化为指针——在 cppreference 上有详细描述;日常你需要的就是上面的操作性规则。参数列表里 classtypename 可互换(template<class T>template<typename T> 等价);现代风格首选 typename

类模板与类型别名

类模板把一个类按类型参数化。语法仍然是 template<typename T> 前缀挂在类定义前:

template<typename T>
class MonteCarloPath {
  public:
    explicit MonteCarloPath(std::vector<T> samples) : samples_(std::move(samples)) {}
    T mean() const {
        T sum = T(0);
        for (const T& x : samples_) sum += x;
        return sum / static_cast<T>(samples_.size());
    }
    std::size_t size() const { return samples_.size(); }
  private:
    std::vector<T> samples_;
};

MonteCarloPath<double>MonteCarloPath<float> 是两个完全独立的类型,它们之间没有隐式转换、没有共同基类、没有共享存储。一个签名为 void summarise(const MonteCarloPath<double>&) 的函数会在编译期拒绝 MonteCarloPath<float> 实参。若要让 summariser 对元素类型也泛化,把它也写成模板:template<typename T> void summarise(const MonteCarloPath<T>&)。编译器在第一次用到时实例化类模板;某个翻译单元若提到 MonteCarloPath<double>,就会把该 T 下的完整类定义(包含每个成员函数体)实例化一遍。

using 形式的类型别名是 typedef 的现代替代,并且与模板友好:

using PriceVector = std::vector<double>;

template<typename T>
using Vec = std::vector<T>;

PriceVector p = {4.20, 4.21, 4.19};
Vec<int> sizes = {1000, 500, 800};

typedef std::vector<double> PriceVector; 对非模板情形仍能工作,但 typedef 无法表达别名模板——语言里没有 typedef template 的语法。using 形式既覆盖非模板别名又覆盖别名模板,且从左到右阅读更顺。新写的代码统一用 using

auto 与结构化绑定

auto 的三种操作形式覆盖了 90% 的日常用法;结构化绑定(C++17)解决「一次取出多个字段」的需求:

auto x = 1.0;                           // double
auto& y = container;                    // reference
const auto& z = compute_price();        // const reference

std::map<std::string, double> prices;
for (const auto& [ticker, price] : prices) {
    // ticker is const std::string&; price is const double&
}

auto x = expr; 从初始化表达式推导值类型。auto& y = container; 推导出引用——少写一个 & 就会拷贝 container(包含 std::vector 自带的一次堆分配);引用形式才是「我打算读写原对象」时该用的写法。const auto& z = expensive_to_copy(); 是「我想读这个值,但不要为拷贝付钱」的经典模式——函数按值或按引用返回而你一时不确定时,先写它。

range-based for 继承同样的形式:for (const auto& x : v) 用于读、for (auto& x : v) 用于改、for (auto x : v) 用于逐个拷贝。结构化绑定一次性把 pair、tuple 或三成员 struct 拆开:auto [key, value] = *map.find(k); 绑定为拷贝;auto& [k, v] = *map.find(k); 绑定为引用;const auto& [k, v] = *it; 是只读形式——你在对 std::map 做 range-based for 时第一想到的就是它。

全特化:紧急出口

当模板的泛型实现对某个具体类型不正确时,「全特化」让你只对该类型重写实现,其它实例化保持泛型。min_of 模板用 < 做比较;当某一实参为 NaN 时,IEEE 754 下 < 会无条件返回前者,结果是错的。修复方式是给一个委托给 std::fmin 的全特化——std::fmin 按 IEEE 754 正确处理 NaN

template<>
double min_of<double>(double a, double b) {
    return std::fmin(a, b);
}

template<> 一行是「这是一个全特化,不是新的主模板」的语法标记。签名指出确切的类型(min_of<double>),与主模板的参数与返回类型对齐。从此 min_of(1.0, 2.0) 走特化版本;min_of(1, 2)min_of(1.0f, 2.0f) 仍走泛型。

全特化适用于「某个类型需要完全不同的实现」。​​偏特化​​——对一族类型做特化,例如 template<typename T> class Box<T*> { ... };——表达力更强,本课不教,留到 3.4.5 的策略框架;那里的 trading-system C++ 代码大量依赖偏特化。SFINAE(std::enable_if)和 C++20 Concepts 也都延后——它们是「如何约束模板」的工具;本课先教模板本体,约束层是后续话题。if constexpr——让你在单个模板定义内按类型分支——是「不必写完全不同实现」时的更轻方案;下一课会讲。

头文件规则

新手最常踩的、且必须记牢的一条规则:​​模板写在头文件里。​ 使用 MonteCarloPath<double> 的翻译单元必须自己实例化完整的类(包括每个成员函数体),而实例化需要模板定义可见。如果把 template<typename T> T mean() const { ... } 放在 monte_carlo_path.cpp,再从 main.cpp 调用 MonteCarloPath<double>::mean()main.cpp 的编译器看不到定义、无从实例化,链接器最终报 undefined reference to MonteCarloPath<double>::mean()

教科书会提到的两条「绕路」对日常代码都是死路。「显式实例化」——在 .cpp 末尾写 template class MonteCarloPath<double>;——对一组已知有限的类型可用,但不可扩展。export template 关键字在 C++11 已被语言移除,不要再去查。务实规则:把模板代码放在 .hpp 头里,谁用就 include 谁,让编译器在每个翻译单元里各自实例化。这意味着模板重的代码编译会更慢,因为每个翻译单元都重复实例化一遍——是的,每一个你将会接触的生产 C++ 代码库都是这样工作的。把这部分编译开销切下来的杠杆在 3.4.3。

串起来:模板化的定价器骨架

worked example 复用 3.4.1 的 CSI 300 ETF(510300.SH)欧式 call 锚定参数:S = 4.20K = 4.30r = 0.024sigma = 0.18T = 0.5,并以 MonteCarloPath<double> 承载 N = 10000 条样本、seed = 42(与 3.3.1 的 Python Monte Carlo 例子一致,便于数值交叉验证):

int main() {
    std::vector<double> samples = generate_paths(/* region-specific S, K, r, sigma, T, N=10000, seed=42 */);
    MonteCarloPath<double> path(std::move(samples));
    std::cout << std::fixed << std::setprecision(4)
              << "mean = " << path.mean() << '\n'
              << "size = " << path.size() << '\n';
    return 0;
}

std::move(samples) 把 vector 的堆存储交给 MonteCarloPath 构造函数,而不发生拷贝——你将在第 3 课亲手实现这套移动语义。std::fixed << std::setprecision(4) 是这门模块里所有数值输出 main 都会出现的固定格式化模式。类按 T 参数化,因此当 3.4.3 做内存压测要从 double 切到 float 时,只需在调用点改一行:MonteCarloPath<float> path(...)

Formula Explorer

(1/N) * sum(payoff(S_T_i))

Exercise

Take the MonteCarloPath<T> class template from this lesson. (a) Add a templated member function T variance() const that computes the sample variance using the formula Var = (1/(N-1)) * sum((x_i - mean)^2) for N >= 2. (b) Add a full template specialization template<> double MonteCarloPath<double>::variance() const that uses std::accumulate from <numeric> and a lambda — the implementation is your choice as long as the answer matches the generic version to within 1e-12 on the same input. (c) Instantiate MonteCarloPath<int> and MonteCarloPath<double> from main, call variance() on each, and observe the integer-division surprise in the int version. State in one sentence why this is a case where the generic does not handle int correctly and a full specialization for double (or a different design — like making variance() always return double) would be the production fix.

提示
样本方差的无偏除数是 N-1。先算 mean,再累加平方偏差。T = int 时小心整数除法。
提示
double 特化,可用一行 std::accumulate(samples_.begin(), samples_.end(), 0.0, [m](double acc, double x){ return acc + (x-m)*(x-m); }) 收掉循环。

下一课

上面这个定价器把 payoff 硬编码在函数体里。真实交易桌会让 payoff 在运行时被参数化——call、put、digital、barrier——并把选定的规则作为可调用对象传入 pricer。下一课引入让这种写法成为惯用法的语言要素:lambda 表达式、其背后的闭包对象、用于做类型擦除存储的 std::function,以及编译期计算所属的 constexpr 家族。