C++ 现代设计模式:CRTP、类型擦除、PIMPL 与策略模式

C++ 的模板和类型系统为经典设计模式提供了独特的实现方式。CRTP 用静态多态替代虚函数开销,类型擦除在保持接口统一的同时抹除具体类型,PIMPL 将实现细节彻底隐藏,策略模式借助模板实现零成本抽象。本文深入这些现代 C++ 设计惯用法,展示如何用编译期计算和类型系统构建高性能、可扩展的架构。

设计模式在 C++ 中从来不是照搬 GoF 的类图。C++ 的模板、 RAII 和零成本抽象哲学,让每个经典模式都有了更具表现力的实现方式。本文聚焦四个在现代 C++ 中高频使用的设计惯用法——CRTP 静态多态、类型擦除、PIMPL 与模板化策略模式,展示编译期多态如何在不牺牲运行时性能的前提下,实现与虚函数表相同乃至更优的架构灵活性。

一、CRTP:奇异递归模板模式

1.1 什么是 CRTP

CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)的核心思想是:派生类将自己作为模板参数传递给基类,从而在编译期建立静态多态关系,完全绕开虚函数表的运行时开销。

template <typename Derived>
class ShapeBase {
public:
    void draw() const {
        static_cast<const Derived*>(this)->draw_impl();
    }
    double area() const {
        return static_cast<const Derived*>(this)->area_impl();
    }
};

class Circle : public ShapeBase<Circle> {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    void draw_impl() const { /* 绘制圆形 */ }
    double area_impl() const { return 3.14159 * radius_ * radius_; }
};

class Rectangle : public ShapeBase<Rectangle> {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    void draw_impl() const { /* 绘制矩形 */ }
    double area_impl() const { return w_ * h_; }
};

注意:基类 ShapeBase<Circle> 和 ShapeBase<Rectangle> 是两个完全不同的类型,不存在共同的基类指针。因此 CRTP 不能用于运行时多态容器(如 std::vector<ShapeBase*>),它适用于编译期已知类型、需要内联优化的场景。

1.2 CRTP vs 虚函数

特性CRTP虚函数
绑定时机编译期运行时
间接调用开销零(完全内联)一次指针间接 + 虚表查找
对象大小增加无每个对象 + vptr(通常 8 字节)
运行时多态不支持支持
二进制膨胀每个实例化一份代码只有一份虚函数实现

1.3 CRTP 实现计数器

CRTP 的经典用途之一是为多个类注入公共功能,而不使用多重继承的虚基类复杂性。

template <typename Derived>
class Counter {
    static inline int count_ = 0;
public:
    Counter() { ++count_; }
    ~Counter() { --count_; }
    Counter(const Counter&) { ++count_; }
    static int count() { return count_; }
};

class Widget : public Counter<Widget> {};
class Gadget : public Counter<Gadget> {};

Widget 和 Gadget 各自拥有独立的计数器,每个类型实例化一份静态成员。这是模板元编程中依赖注入的简洁形式——与 https://plumephp.com/cpp-metaprogramming/ 中讨论的编译期技巧一脉相承。

1.4 CRTP 的局限

  • 无法将 CRTP 对象放入异构容器(除非外层加一层类型擦除);
  • 复杂的 CRTP 继承层次会导致模板错误信息晦涩;
  • 编译时间随实例化数量增加。

二、类型擦除(Type Erasure)

当需要运行时多态,但又不想引入虚函数表的 ABI 约束或二进制接口依赖时,类型擦除提供了一条中间道路。它抹除具体类型,只暴露统一接口,内部通过虚函数表或函数指针转发调用。

2.1 手搓类型擦除:手写虚函数表

#include <memory>
#include <vector>
#include <iostream>

class Drawable {
    struct Concept {
        virtual ~Concept() = default;
        virtual void draw() const = 0;
        virtual std::unique_ptr<Concept> clone() const = 0;
    };

    template <typename T>
    struct Model : Concept {
        T data_;
        explicit Model(T data) : data_(std::move(data)) {}
        void draw() const override { data_.draw(); }
        std::unique_ptr<Concept> clone() const override {
            return std::make_unique<Model<T>>(data_);
        }
    };

    std::unique_ptr<Concept> impl_;

public:
    template <typename T>
    explicit Drawable(T obj) : impl_(std::make_unique<Model<T>>(std::move(obj))) {}

    Drawable(const Drawable& other) : impl_(other.impl_->clone()) {}
    Drawable(Drawable&&) = default;
    Drawable& operator=(const Drawable& other) {
        impl_ = other.impl_->clone();
        return *this;
    }
    Drawable& operator=(Drawable&&) = default;

    void draw() const { impl_->draw(); }
};

Drawable 是一个值语义的多态句柄。它可以容纳任何拥有 draw() 方法的对象,而不暴露具体的类型信息:

struct Circle {
    void draw() const { std::cout << "Drawing Circle\n"; }
};
struct Square {
    void draw() const { std::cout << "Drawing Square\n"; }
};

int main() {
    std::vector<Drawable> shapes;
    shapes.emplace_back(Circle{});
    shapes.emplace_back(Square{});
    for (const auto& s : shapes) s.draw();
}

2.2 std::any 与 std::variant

标准库也提供了两种受限的类型擦除工具。

std::any 可以存储任意类型的一个值,但在取出时需要知道原始类型:

#include <any>
#include <string>

std::any value = 42;
if (value.type() == typeid(int)) {
    int n = std::any_cast<int>(value);
}
value = std::string("hello");

std::variant<T1, T2, ...> 是一个类型安全的联合体,只能存储预先声明的类型之一,配合 std::visit 可实现分派:

#include <variant>
#include <vector>
#include <iostream>

using ShapeVariant = std::variant<Circle, Square, Triangle>;

void draw_shape(const ShapeVariant& s) {
    std::visit([](const auto& shape) {
        shape.draw();
    }, s);
}

int main() {
    std::vector<ShapeVariant> shapes;
    shapes.emplace_back(Circle{});
    shapes.emplace_back(Square{});
    for (const auto& s : shapes) draw_shape(s);
}

std::variant 不使用堆分配,所有可能的类型共享同一块存储空间(大小取最大类型),并附加一个类型标签。它特别适合状态机、解析结果等场景——在 https://plumephp.com/cpp-modern-17-20-23/ 中对 std::variant 有更深入的讨论。

三、PIMPL:指针指向实现

3.1 PIMPL 惯用法

PIMPL(Pointer to IMPLementation)是 C++ 中最经典的编译防火墙技术。它将类的私有成员和实现细节隐藏在 .cpp 文件中,.h 头文件中只暴露一个不透明指针。

// widget.h —— 暴露的接口
#include <memory>

class Widget {
public:
    Widget();
    ~Widget();
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;

    void do_something();
    int get_value() const;

private:
    class Impl;           // 前向声明,不暴露细节
    std::unique_ptr<Impl> pImpl;
};
// widget.cpp —— 实现细节
#include "widget.h"
#include <vector>
#include <string>

class Widget::Impl {
public:
    std::vector<int> data_;
    std::string name_;
    int cache_ = 0;

    void heavy_computation() {
        // 复杂实现...
    }
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;

void Widget::do_something() {
    pImpl->heavy_computation();
}

int Widget::get_value() const {
    return pImpl->cache_;
}

3.2 PIMPL 的收益

  • 编译隔离:修改私有成员不会导致依赖该头文件的所有翻译单元重新编译;
  • ABI 稳定:库的公开接口不变时,可以安全地更换实现(对于共享库尤为重要);
  • 隐藏依赖:实现中使用的第三方库(如某个 JSON 解析器)不会污染头文件。

3.3 权衡

PIMPL 的代价是每个对象多一次堆分配和指针间接访问。对于轻量对象或需要极致缓存友好的场景(见 https://plumephp.com/cpp-performance-optimization/),应评估 PIMPL 是否真的必要。一个折中方案是 std::aligned_storage 内联存储小型_IMPL 对象,避免堆分配。

四、模板化策略模式与空基类优化

4.1 模板策略 vs 继承策略

经典策略模式使用运行时多态:

class CompressionStrategy {
public:
    virtual ~CompressionStrategy() = default;
    virtual std::vector<uint8_t> compress(const std::vector<uint8_t>& data) = 0;
};

class Compressor {
    std::unique_ptr<CompressionStrategy> strategy_;
public:
    void set_strategy(std::unique_ptr<CompressionStrategy> s) {
        strategy_ = std::move(s);
    }
    std::vector<uint8_t> compress(const std::vector<uint8_t>& data) {
        return strategy_->compress(data);
    }
};

模板化版本将策略绑定在编译期,消除虚函数开销:

template <typename Strategy>
class Compressor {
    Strategy strategy_;
public:
    std::vector<uint8_t> compress(const std::vector<uint8_t>& data) {
        return strategy_.compress(data);
    }
};

struct GzipStrategy {
    std::vector<uint8_t> compress(const std::vector<uint8_t>& data) {
        // Gzip 实现...
        return data;
    }
};

// 使用:Compressor<GzipStrategy> comp;

4.2 空基类优化(EBO)

当策略类是无状态的(只有静态方法),模板实例化后它可能是一个空类(大小为 1,C++ 规定对象至少 1 字节)。通过私有继承而非成员变量持有策略,可以利用空基类优化(Empty Base Optimization, EBO),让空基类不占用空间:

template <typename Strategy>
class Compressor : private Strategy {  // 私有继承以获得 EBO
public:
    std::vector<uint8_t> compress(const std::vector<uint8_t>& data) {
        // 直接调用基类方法
        return Strategy::compress(data);
    }
};

struct NoOpStrategy {
    static std::vector<uint8_t> compress(const std::vector<uint8_t>& data) {
        return data;
    }
};

static_assert(sizeof(Compressor<NoOpStrategy>) == 1);  // EBO 生效,只占用 1 字节标记

EBO 是 C++ 独有的优化手段,也是 std::tuple 和多个标准库容器的实现基础。需要注意的是,同一类型的多个基类不能同时存在——Compressor<T, T> 的 EBO 会失效。

4.3 访问者模式与 std::variant

访问者模式在 C++17/20 中与 std::variant 和泛型 lambda 结合,得到了革命性的简化:

#include <variant>
#include <iostream>

struct Circle { double radius; };
struct Rectangle { double w, h; };

using Shape = std::variant<Circle, Rectangle>;

// 泛型访问者(C++14 起支持泛型 lambda)
struct AreaVisitor {
    double operator()(const Circle& c) const {
        return 3.14159 * c.radius * c.radius;
    }
    double operator()(const Rectangle& r) const {
        return r.w * r.h;
    }
};

int main() {
    Shape s1 = Circle{5.0};
    Shape s2 = Rectangle{3.0, 4.0};

    std::cout << std::visit(AreaVisitor{}, s1) << std::endl;
    std::cout << std::visit(AreaVisitor{}, s2) << std::endl;

    // C++14 泛型 lambda
    auto print = [](const auto& shape) {
        std::cout << "Area: " << std::visit(AreaVisitor{}, Shape{shape}) << std::endl;
    };
}

std::visit 在编译期为所有 variant 的替代类型生成分派表,性能与手写 switch 语句相当,但类型安全由编译器保证。

相关阅读

  • https://plumephp.com/cpp-templates-generics/ — 函数模板、偏特化与类型萃取的基础
  • https://plumephp.com/cpp-metaprogramming/ — 编译期计算与类型列表的深度技巧
  • https://plumephp.com/cpp-modern-17-20-23/ — std::variant、std::any 与结构化绑定配合模式

延伸阅读

  • https://plumephp.com/posts/cs-fundamentals/ — 设计模式的 GoF 经典定义与理论基础
  • https://plumephp.com/posts/hpc/ — 高性能计算中模板元编程与零成本抽象的工程实践
  • https://plumephp.com/posts/graphics/ — 游戏引擎中 ECS 架构与类型擦除的应用

文末完整示例

// 完整可运行示例:CRTP + 类型擦除 + PIMPL + 策略模式
// 编译:g++ -std=c++20 -o design_patterns_demo design_patterns_demo.cpp

#include <iostream>
#include <vector>
#include <memory>
#include <variant>

// ====== CRTP 静态多态 ======
template <typename Derived>
class ShapeCRTP {
public:
    double area() const {
        return static_cast<const Derived*>(this)->area_impl();
    }
    void describe() const {
        static_cast<const Derived*>(this)->describe_impl();
    }
};

class CircleCRTP : public ShapeCRTP<CircleCRTP> {
    double r_;
public:
    explicit CircleCRTP(double r) : r_(r) {}
    double area_impl() const { return 3.14159 * r_ * r_; }
    void describe_impl() const { std::cout << "Circle(r=" << r_ << ")"; }
};

class RectCRTP : public ShapeCRTP<RectCRTP> {
    double w_, h_;
public:
    RectCRTP(double w, double h) : w_(w), h_(h) {}
    double area_impl() const { return w_ * h_; }
    void describe_impl() const { std::cout << "Rect(" << w_ << "x" << h_ << ")"; }
};

// ====== 手搓类型擦除(值语义) ======
class Drawable {
    struct Concept {
        virtual ~Concept() = default;
        virtual void draw() const = 0;
        virtual std::unique_ptr<Concept> clone() const = 0;
    };
    template <typename T>
    struct Model : Concept {
        T data_;
        explicit Model(T d) : data_(std::move(d)) {}
        void draw() const override { data_.draw(); }
        std::unique_ptr<Concept> clone() const override {
            return std::make_unique<Model<T>>(data_);
        }
    };
    std::unique_ptr<Concept> impl_;
public:
    template <typename T>
    explicit Drawable(T obj) : impl_(std::make_unique<Model<T>>(std::move(obj))) {}
    Drawable(const Drawable& o) : impl_(o.impl_->clone()) {}
    void draw() const { impl_->draw(); }
};

struct CircleDrawable { void draw() const { std::cout << "Drawing Circle\n"; } };
struct SquareDrawable { void draw() const { std::cout << "Drawing Square\n"; } };

// ====== PIMPL 演示 ======
class Widget {
    class Impl;
    std::unique_ptr<Impl> pImpl;
public:
    Widget();
    ~Widget();
    void configure(int x);
    int get() const;
};

class Widget::Impl {
public:
    int value_ = 0;
    void configure(int x) { value_ = x * 2; }
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
void Widget::configure(int x) { pImpl->configure(x); }
int Widget::get() const { return pImpl->value_; }

// ====== 模板策略 + EBO ======
template <typename Strategy>
class Processor : private Strategy {
public:
    int process(int x) {
        return Strategy::compute(x);
    }
};

struct DoubleStrategy {
    static int compute(int x) { return x * 2; }
};
struct SquareStrategy {
    static int compute(int x) { return x * x; }
};

// ====== variant 访问者 ======
struct CircleV { double r; };
struct RectV { double w, h; };
using ShapeV = std::variant<CircleV, RectV>;

struct AreaVisitor {
    double operator()(const CircleV& c) const { return 3.14159 * c.r * c.r; }
    double operator()(const RectV& r) const { return r.w * r.h; }
};

int main() {
    // CRTP
    CircleCRTP c(3.0);
    RectCRTP r(4.0, 5.0);
    std::cout << "=== CRTP ===" << std::endl;
    c.describe(); std::cout << " area=" << c.area() << std::endl;
    r.describe(); std::cout << " area=" << r.area() << std::endl;

    // 类型擦除
    std::cout << "\n=== Type Erasure ===" << std::endl;
    std::vector<Drawable> drawables;
    drawables.emplace_back(CircleDrawable{});
    drawables.emplace_back(SquareDrawable{});
    for (const auto& d : drawables) d.draw();

    // PIMPL
    std::cout << "\n=== PIMPL ===" << std::endl;
    Widget w;
    w.configure(5);
    std::cout << "Widget value: " << w.get() << std::endl;

    // 模板策略
    std::cout << "\n=== Strategy ===" << std::endl;
    Processor<DoubleStrategy> pd;
    Processor<SquareStrategy> ps;
    std::cout << "Double(7)=" << pd.process(7)
              << " Square(7)=" << ps.process(7) << std::endl;

    // Variant 访问者
    std::cout << "\n=== Variant ===" << std::endl;
    ShapeV s1 = CircleV{2.0};
    ShapeV s2 = RectV{3.0, 4.0};
    std::cout << "Circle area=" << std::visit(AreaVisitor{}, s1) << std::endl;
    std::cout << "Rect area=" << std::visit(AreaVisitor{}, s2) << std::endl;

    return 0;
}

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「cpp」更多文章

  1. C++20 Modules 模块化与构建系统演进
  2. C++20 Ranges 与惰性求值视图
  3. C++ 单元测试框架:Google Test、Catch2、Doctest 与 Mock 技巧