跳至内容

模板元编程模式

掌握高级模板模式:用于静态多态的 CRTP、用于灵活组合的策略式设计、用于运行时泛型的类型擦除,以及何时使用模板与虚分发的指导原则。

超越基本模板

模板支持在其他语言中没有等价物的设计模式。这些模式利用模板的编译时特性来实现静态多态(没有虚函数开销)、可组合的行为(策略)和类型安全的擦除(将具体类型隐藏在统一接口后面)。

这些不是初学者模式 ── 它们是库作者和框架设计者的工具。理解它们对于阅读标准库内部、Boost 和现代 C++ 框架至关重要。

CRTP:奇异递归模板模式

在 CRTP 中,一个类从以自身作为参数实例化的模板派生:class Derived : public Base。基类可以在编译时调用派生类的方法 ── 没有虚分发开销。这用于注入行为、实现静态多态和创建 mixin 功能。

crtp.cpp
#include <iostream>

// CRTP base: provides a counter for each derived class
template <typename Derived>
class Counter {
    static inline int count_ = 0;

protected:
    Counter()  { ++count_; }
    ~Counter() { --count_; }
    Counter(const Counter&) { ++count_; }

public:
    static int alive() { return count_; }
};

// Each derived class gets its OWN counter
class Dog : public Counter<Dog> {
public:
    std::string name;
    Dog(std::string n) : name(std::move(n)) {}
};

class Cat : public Counter<Cat> {
public:
    std::string name;
    Cat(std::string n) : name(std::move(n)) {}
};

// CRTP for static polymorphism: no virtual dispatch
template <typename Derived>
class Shape {
public:
    double area() const {
        // Call derived class method — resolved at compile time
        return static_cast<const Derived*>(this)->area_impl();
    }
};

class Circle : public Shape<Circle> {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double area_impl() const { return 3.14159265 * radius_ * radius_; }
};

class Square : public Shape<Square> {
    double side_;
public:
    Square(double s) : side_(s) {}
    double area_impl() const { return side_ * side_; }
};

int main() {
    Dog d1("Rex"), d2("Buddy");
    Cat c1("Whiskers");
    std::cout << "Dogs alive: " << Dog::alive() << '\n'; // 2
    std::cout << "Cats alive: " << Cat::alive() << '\n'; // 1

    Circle circle(5.0);
    Square square(4.0);
    std::cout << "Circle area: " << circle.area() << '\n'; // 78.54
    std::cout << "Square area: " << square.area() << '\n'; // 16.0
}

策略式设计

策略式设计将类分解为作为模板参数传递的可互换行为组件(策略)。每个策略实现行为的一个方面。用户组合出他们需要的确切组合。这由 Andrei Alexandrescu 的《Modern C++ Design》推广。

policy_design.cpp
#include <iostream>
#include <mutex>
#include <stdexcept>

// Threading policies
struct SingleThreaded {
    struct Lock { Lock(SingleThreaded&) {} }; // no-op
};

struct MultiThreaded {
    std::mutex mtx;
    struct Lock {
        std::lock_guard<std::mutex> guard;
        Lock(MultiThreaded& host) : guard(host.mtx) {}
    };
};

// Bounds checking policies
struct NoBoundsCheck {
    static void check(int /*index*/, int /*size*/) {} // no-op
};

struct StrictBoundsCheck {
    static void check(int index, int size) {
        if (index < 0 || index >= size)
            throw std::out_of_range("Index out of bounds");
    }
};

// Policy-based container: users choose their combination
template <typename T,
         typename ThreadingPolicy = SingleThreaded,
         typename BoundsPolicy = NoBoundsCheck>
class SmartArray : private ThreadingPolicy {
    T* data_;
    int size_;

public:
    SmartArray(int size) : data_(new T[size]{}), size_(size) {}
    ~SmartArray() { delete[] data_; }

    T& operator[](int index) {
        typename ThreadingPolicy::Lock lock(*this);
        BoundsPolicy::check(index, size_);
        return data_[index];
    }

    int size() const { return size_; }
};

int main() {
    // Fast, no overhead: no locking, no bounds checks
    SmartArray<int> fast(10);
    fast[0] = 42;

    // Safe: bounds-checked, single-threaded
    SmartArray<int, SingleThreaded, StrictBoundsCheck> safe(10);
    safe[0] = 42;
    try {
        safe[99] = 0; // throws!
    } catch (const std::out_of_range& e) {
        std::cout << e.what() << '\n';
    }

    // Thread-safe with bounds checking
    SmartArray<int, MultiThreaded, StrictBoundsCheck> robust(10);
    robust[5] = 100;
}

类型擦除

类型擦除将具体类型隐藏在统一接口后面,结合了模板的灵活性和虚函数的运行时多态性。经典的例子是 std::function,它可以持有任何可调用对象。该模式使用三个组件:一个概念(抽象接口)、一个模型(为任何类型实现接口的模板包装器)和一个外部句柄类。

type_erasure.cpp
#include <iostream>
#include <memory>
#include <string>

// Type-erased "Printable" — can hold any type with a print() method
class Printable {
    // Concept: abstract interface
    struct Concept {
        virtual ~Concept() = default;
        virtual void print(std::ostream& os) const = 0;
        virtual std::unique_ptr<Concept> clone() const = 0;
    };

    // Model: wraps any T that supports operator<<
    template <typename T>
    struct Model : Concept {
        T value;
        Model(T v) : value(std::move(v)) {}
        void print(std::ostream& os) const override { os << value; }
        std::unique_ptr<Concept> clone() const override {
            return std::make_unique<Model>(value);
        }
    };

    std::unique_ptr<Concept> ptr_;

public:
    // Constructor: accepts any printable type
    template <typename T>
    Printable(T value) : ptr_(std::make_unique<Model<T>>(std::move(value))) {}

    // Copy support
    Printable(const Printable& other) : ptr_(other.ptr_->clone()) {}
    Printable& operator=(const Printable& other) {
        ptr_ = other.ptr_->clone();
        return *this;
    }
    Printable(Printable&&) = default;
    Printable& operator=(Printable&&) = default;

    friend std::ostream& operator<<(std::ostream& os, const Printable& p) {
        p.ptr_->print(os);
        return os;
    }
};

int main() {
    // Heterogeneous collection — no common base class needed!
    std::vector<Printable> items;
    items.emplace_back(42);
    items.emplace_back(3.14);
    items.emplace_back(std::string("hello"));
    items.emplace_back('X');

    for (const auto& item : items) {
        std::cout << item << '\n';
    }
    // Output: 42, 3.14, hello, X
}

表达式模板(概览)

表达式模板是一种技术,其中对象上的算术表达式构建一个编译时操作树,而不是计算中间结果。最终结果在单次遍历中计算,消除了临时对象。

例如,使用朴素的 Vector 类,a + b + c 创建两个临时向量。使用表达式模板,a + b + c 构建一个轻量的表达式对象 Add, Vector>,按需计算元素 ── 不需要临时对象。

这种技术被高性能线性代数库如 Eigen 和 Blaze 使用。表达式树被惰性求值,编译器通常可以对生成的单次遍历循环进行向量化。

表达式模板的正确实现很复杂,主要是库作者的关注点。如果你需要矩阵/向量数学运算,使用 Eigen 等库,而不是自己实现表达式模板。

最佳实践

在模板(静态多态)和虚函数(动态多态)之间选择时:

使用模板当:
- 类型集合在编译时已知
- 你需要零开销抽象(没有虚表、没有间接调用)
- 你希望编译器跨类型边界内联和优化
- 你需要使用值语义(无堆分配)
- 性能至关重要(紧密循环、数值代码)

使用虚函数当:
- 类型集合是开放的且在运行时确定(插件、用户输入)
- 你需要异构容器(例如 vector>)
- 二进制大小比速度更重要(模板生成更多代码)
- 你需要跨共享库边界的稳定 ABI
- 你需要在不重新编译现有代码的情况下添加新类型

使用类型擦除当:
- 你想要虚分发的接口简洁性,但不想强制用户从你的基类继承
- 你需要具有运行时多态性的值语义(std::function、std::any)

常见陷阱

模板元编程强大但有实际代价:

1. 编译时爆炸 ── 深度嵌套的模板实例化可能使编译时间变得不可接受。递归 TMP 尤其糟糕 ── 优先使用折叠表达式和 if constexpr。
2. 错误信息 ── 没有概念的情况下,模板错误会产生数页难以理解的文本。始终约束模板。
3. 调试 ── 在调试器中单步执行模板密集的代码很痛苦。类型擦除包装器添加了隐藏实际类型的间接层。
4. 可读性 ── CRTP、策略式设计和表达式模板对初级开发者来说很难理解。记录模式名称和意图。
5. 过度工程 ── 不是每个问题都需要模板解决方案。简单的虚函数甚至 switch 语句可能更清晰且足够。

关键要点
  • CRTP 通过从 Base 派生来实现静态多态和每类计数器/mixin
  • 策略式设计将行为分解为模板参数 ── 用户组合出他们需要的确切组合
  • 类型擦除结合了模板的灵活性和运行时多态性(例如 std::function、std::any)
  • 表达式模板消除链式操作中的临时对象 ── 被 Eigen、Blaze 等库使用
  • 已知类型且需要零开销时选择模板;开放类型集和运行时灵活性时选择虚分发
  • 避免过度工程 ── 使用满足需求的最简单机制

测验 — 检验你的知识

(20 经验值)

1. 在 CRTP 中,`class Dog : public Counter<Dog>` 实现了什么?

2. 类型擦除解决什么问题?

3. 什么时候应该优先使用虚函数而非模板?