第二十五章 设计模式:现代 C++ 的惯用法

← 返回目录

本章目标:掌握在 C++ 中最常用、最有价值的设计模式, 重点是**现代 C++ 如何用更简单的语言特性实现它们** (很多"四人帮"模式在 C++11 之后有了更优雅的替代)。

25.1 为什么还要学设计模式

设计模式 = 前人总结的"常见问题的标准解法"。 但注意:**模式是工具,不是目标**。模式很多, 这里只讲 C++ 程序员真正高频使用的 8 个。

现代 C++ 的立场:

25.2 工厂模式:创建对象的"中央厨房"

场景:根据配置/参数创建不同类型的对象,调用方不用知道 具体类型。多态 + 智能指针的经典组合。

class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};
class Circle : public Shape { ... };
class Rect   : public Shape { ... };

enum class ShapeKind { Circle, Rect };

std::unique_ptr<Shape> make_shape(ShapeKind kind, double a, double b) {
    switch (kind) {
        case ShapeKind::Circle: return std::make_unique<Circle>(a);
        case ShapeKind::Rect:   return std::make_unique<Rect>(a, b);
    }
    throw std::runtime_error("未知图形类型");
}

// 用法:只跟 Shape 打交道
auto s = make_shape(ShapeKind::Circle, 3.0, 0.0);
std::println("面积 {}", s->area());

现代替代:很多"简单工厂"可以用 variant 替代(类型全 已知时),见 25.8。

25.3 单例模式:全局唯一对象(谨慎使用)

场景:日志、配置、数据库连接池等全局唯一的资源。

现代 C++ 最简实现(Meyers Singleton):

class Logger {
public:
    static Logger& instance() {        // C++11 起线程安全的
        static Logger log;             // 首次调用时构造,且只构造一次
        return log;
    }
    void log(std::string_view msg) { std::println("[日志] {}", msg); }
    Logger(const Logger&) = delete;            // 禁止拷贝
    Logger& operator=(const Logger&) = delete;
private:
    Logger() = default;                // 禁止外部构造
};

Logger::instance().log("应用启动");

为什么现代 C++ 更推荐"显式传递"而不是单例:

  1. 单例 = 隐藏的全局状态 → 难测试、难并行
  2. 依赖关系不透明(谁都用它,谁都改它)
  3. 初始化顺序问题(多个单例互相依赖时)

建议:小工具(日志)可以用;重要的业务对象用依赖注入。

25.4 观察者模式:事件通知(lambda 版最优雅)

场景:一个对象状态变化时,通知一组"听众"。

现代 C++ 用 std::function + 容器实现,几十行搞定:

class EventBus {
public:
    using Handler = std::function<void(int)>;
    void subscribe(Handler h) { handlers_.push_back(std::move(h)); }
    void publish(int event) {
        for (auto& h : handlers_) h(event);
    }
private:
    std::vector<Handler> handlers_;
};

EventBus bus;
bus.subscribe([](int e) { std::println("监听者1收到 {}", e); });
bus.subscribe([](int e) { std::println("监听者2收到 {}", e); });
bus.publish(42);                     // 两个监听者都被通知

关键点:订阅者用 lambda,不用继承 Observer 接口—— 比"四人帮"原版简洁得多。这就是现代 C++ 的设计进化。

25.5 RAII 守卫模式(C++ 最独特的模式)

场景:进入某段代码时做 X,离开时(无论正常还是异常)做 Y。

class TimerGuard {
public:
    TimerGuard(const char* name)
        : name_(name), start_(std::chrono::steady_clock::now()) {}
    ~TimerGuard() {
        auto ms = std::chrono::duration<double, std::milli>(
            std::chrono::steady_clock::now() - start_).count();
        std::println("{}: {:.2f} ms", name_, ms);
    }
private:
    const char* name_;
    std::chrono::steady_clock::time_point start_;
};

void work() {
    TimerGuard t("work 耗时");   // 进入
    // ... 任何代码,包括抛异常 ...
}   // 离开时自动打印耗时

这是 RAII 模式(scope guard)——在析构里自动执行清理, 是最"C++ 风格"的模式。C++26 有 std::scope_exit 官方版本。

25.6 策略模式:算法可插拔(lambda 天然实现)

场景:同一操作有多种算法,运行时选择。

// 排序策略
using SortStrategy = std::function<std::vector<int>(std::vector<int>)>;

std::vector<int> sort_asc(std::vector<int> v) {
    std::ranges::sort(v);
    return v;
}
std::vector<int> sort_desc(std::vector<int> v) {
    std::ranges::sort(v, std::greater<>{});
    return v;
}

std::vector<int> apply(SortStrategy s, std::vector<int> v) {
    return s(std::move(v));
}

std::vector<int> data{3, 1, 2};
auto asc = apply(sort_asc, data);
auto desc = apply(sort_desc, data);

其实大部分"策略"就是传个 lambda 参数:

std::ranges::sort(v, [](int a, int b) { return a % 10 < b % 10; });

25.7 CRTP:编译期多态(模板时代的高级模式)

CRTP = Curiously Recurring Template Pattern (奇异递归模板模式):类继承自己的模板化父类。

template <typename Derived>
class Base {
public:
    void interface() {                          // 非虚"虚函数"
        static_cast<Derived*>(this)->impl();
    }
};

class Foo : public Base<Foo> {
public:
    void impl() { std::println("Foo 的实现"); }
};
class Bar : public Base<Bar> {
public:
    void impl() { std::println("Bar 的实现"); }
};

Foo f; Bar b;
f.interface();      // 调 Foo::impl(编译期决定)
b.interface();      // 调 Bar::impl

用途:

  1. 没有虚函数开销的多态(游戏引擎/高性能代码)
  2. 编译期接口约束("必须实现 impl")
  3. mixin 组合功能(给类附加能力)

比虚函数快(无 vtable),但类型必须是编译期已知的。 C++23 的 deducing this 可以简化 CRTP 的写法(见第十三章)。

25.8 用 variant 代替"继承树"(现代替代方案)

当"类型集合固定且不大"时,variant 比继承更简单更快:

struct Circle { double r; };
struct Rect   { double w, h; };
using Shape = std::variant<Circle, Rect>;

double area(const Shape& s) {
    return std::visit([](const auto& sh) {
        using T = std::decay_t<decltype(sh)>;
        if constexpr (std::is_same_v<T, Circle>) return 3.14159 * sh.r * sh.r;
        else return sh.w * sh.h;
    }, s);
}

Shape s1 = Circle{2.0};
Shape s2 = Rect{3.0, 4.0};
CHECK(area(s1) ≈ 12.57);  // 概念示意

什么时候用继承,什么时候用 variant?

25.9 依赖注入:让代码可测试

人话:需要什么就"传进来",而不是"自己 new"。

// 反模式:硬编码依赖,没法测试
class OrderService {
    Database db_;          // 内部 new,测试时没法替换
};

// 注入:依赖由外部提供
class OrderService {
public:
    explicit OrderService(Database& db) : db_(db) {}
private:
    Database& db_;         // 引用注入
};

// 测试时传入 mock
class MockDb : public Database { ... };
MockDb mock;
OrderService svc(mock);

好处:单测友好、依赖清晰、耦合低。

25.10 陷阱清单

  1. 为模式而模式:先写简单代码,问题出现再引入模式
  2. 单例滥用(全局状态 → 难测试)
  3. 工厂返回裸指针(用 unique_ptr,RAII 管理)
  4. 观察者忘了退订 → 悬空(列表里存了已销毁对象的函数)
  5. CRTP 用 static_cast 时写错类型(必须 cast 到 Derived)
  6. 用继承实现"只是想复用代码"的类(应组合/模板)
  7. 模式实现里泄漏资源(一切用 RAII)

本章小结

练习题(配套测试:测试_第25章_设计模式.cpp)


  1. 实现工厂创建 Shape 并计算总面积。
  2. 实现 Meyers 单例日志器。
  3. 实现事件总线(3 个订阅者)。
  4. 实现 RAII 计时守卫。
  5. 用 variant 实现 Shape 并计算面积(不写继承)。