设计模式在 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;
}
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。