29. C++11/14/17 新特性对设计模式的影响:智能指针与RAII、lambda表达式、移动语义、std::function与std::bind

说实话,C++11 之前的经典设计模式,写起来总有点「拧巴」的感觉。你想想看,GoF 那本书里,到处都是手动 new 和 delete,到处都是函数指针和回调注册。那时候写代码,一半精力在跟内存管理搏斗,另一半才真正在思考业务逻辑。

但 C++11 之后,情况完全变了。智能指针、lambda、移动语义这些东西,不是锦上添花,而是从根本上改变了我们实现设计模式的方式。我这些年重构过不少老项目,最深的一个体会就是:新特性让设计模式变得更安全、更简洁、更贴近直觉

下面我挑四个最关键的「杀手级特性」,聊聊它们怎么重塑了经典设计模式。

29.1 智能指针与RAII:让内存管理「隐形」

先问个问题:你写工厂模式的时候,最怕什么?

我猜是「谁负责销毁对象」。传统工厂返回原始指针,调用方得记着 delete。万一中间抛异常,或者调用方忘了释放,内存泄漏就来了。

智能指针完美解决了这个问题。说白了,就是把「所有权」和「生命周期」这两个概念,用类型系统明确表达出来。

核心变化:

  • std::unique_ptr:独占所有权,适合工厂模式、组合模式中的独占关系
  • std::shared_ptr:共享所有权,适合观察者模式、享元模式中的共享资源
  • std::weak_ptr:打破循环引用,适合观察者模式中避免 dangling pointer

我在项目中遇到过这样一个场景:一个老旧的观察者模式实现,Subject 持有 Observer 的原始指针列表。Observer 析构时,得手动从 Subject 里把自己移除。一旦漏了,就是野指针崩溃。后来改成 shared_ptr + weak_ptr,问题迎刃而解。

// 传统方式:手动管理,容易泄漏
class Subject {
    std::vector<Observer*> observers_;
public:
    void attach(Observer* obs) { observers_.push_back(obs); }
    void notify() {
        for (auto* obs : observers_) obs->update();
    }
    // 忘记提供 detach?或者 observer 析构时没调用?等着崩溃吧
};

// 现代方式:RAII 自动管理
class Subject {
    std::vector<std::weak_ptr<Observer>> observers_;
public:
    void attach(std::shared_ptr<Observer> obs) {
        observers_.push_back(obs);
    }
    void notify() {
        // 自动过滤已失效的观察者
        for (auto& weak : observers_) {
            if (auto obs = weak.lock()) {
                obs->update();
            }
        }
    }
};

我的习惯: 只要涉及「对象生命周期跨越当前作用域」,一律用智能指针。原始指针只用于「非拥有」的观察,而且必须保证被观察对象的生命周期更长。

29.2 lambda 表达式:策略模式与命令模式的「语法糖革命」

lambda 表达式是我个人最爱的 C++11 特性,没有之一。为什么?因为它让「行为参数化」变得极其自然。

回想一下传统的策略模式:你得定义一个抽象策略接口,然后写一堆具体策略类,每个类里就一个方法。代码量翻倍,可读性却下降。

有了 lambda,策略模式可以直接「内联」实现。命令模式也是同理——以前要写一堆 Command 子类,现在一个 lambda 搞定。

// 传统策略模式:需要定义接口和多个类
struct SortStrategy {
    virtual void sort(std::vector<int>& data) = 0;
    virtual ~SortStrategy() = default;
};
struct QuickSort : SortStrategy { /* ... */ };
struct MergeSort : SortStrategy { /* ... */ };

// 现代方式:lambda 即策略
class Sorter {
    std::function<void(std::vector<int>&)> strategy_;
public:
    void setStrategy(std::function<void(std::vector<int>&)> s) {
        strategy_ = std::move(s);
    }
    void execute(std::vector<int>& data) {
        if (strategy_) strategy_(data);
    }
};

// 使用:想怎么排就怎么排
Sorter s;
s.setStrategy([](auto& v) { std::sort(v.begin(), v.end()); });
s.setStrategy([](auto& v) { std::sort(v.rbegin(), v.rend()); });

你想想看,以前写命令模式,每个命令都得单独一个类。现在呢?

// 命令模式:lambda 即命令
std::vector<std::function<void()>> commands;
commands.push_back([] { std::cout << "打开文件\n"; });
commands.push_back([] { std::cout << "读取数据\n"; });
commands.push_back([] { std::cout << "关闭文件\n"; });

for (auto& cmd : commands) cmd();

注意: lambda 默认按值捕获,小心悬空引用。如果 lambda 要异步执行,确保捕获的变量生命周期足够长。我见过太多「lambda 里捕获了局部变量的引用,结果 lambda 执行时变量已经销毁」的 bug。

29.3 移动语义:让性能不再是「拷贝的代价」

移动语义的出现,让很多设计模式在性能上有了质的飞跃。尤其是那些涉及「对象传递」的模式——工厂模式、原型模式、构建器模式。

以前写工厂模式,返回一个大型对象,要么返回指针(增加内存管理负担),要么拷贝(性能灾难)。现在呢?直接返回值,移动语义自动生效。

class BigObject {
    std::vector<double> data_;  // 假设有 100 万个元素
public:
    BigObject() : data_(1'000'000) {}
    // 移动构造自动生成(C++11 起)
    BigObject(BigObject&&) = default;
};

// 工厂函数:返回大型对象,零拷贝
BigObject createBigObject() {
    BigObject obj;
    // ... 配置 obj
    return obj;  // 编译器自动使用移动语义,甚至 NRVO 优化
}

原型模式也是同理。以前克隆一个对象,得手动实现 clone 方法,里面做深拷贝。现在有了移动语义,我们可以区分「克隆(深拷贝)」和「移动(转移所有权)」,让调用方按需选择。

关键点: 移动语义不是「优化」,而是「语义明确」。它告诉读者:这个对象我要转移,不再需要了。这种语义上的清晰,比性能提升更重要。

29.4 std::function 与 std::bind:解耦的终极武器

std::function 是个「万能函数包装器」。它可以装 lambda、函数指针、成员函数、仿函数……任何可调用对象。这玩意儿对设计模式的影响,怎么说呢,就像胶水一样,把各个组件粘在一起,但又保持了解耦。

观察者模式里,以前 Subject 得知道 Observer 的具体类型。现在呢?Subject 只需要存一个 std::function 列表,Observer 可以是任何东西——一个 lambda、一个成员函数、甚至一个仿函数。

class EventBus {
    std::vector<std::function<void(const Event&)>> handlers_;
public:
    // 订阅者可以是任何可调用对象
    template<typename F>
    void subscribe(F&& handler) {
        handlers_.emplace_back(std::forward<F>(handler));
    }

    void publish(const Event& event) {
        for (auto& handler : handlers_) handler(event);
    }
};

// 使用:完全解耦
EventBus bus;
bus.subscribe([](const Event& e) { /* 处理事件 */ });

MyClass obj;
bus.subscribe(std::bind(&MyClass::onEvent, &obj, std::placeholders::_1));

std::bind 虽然现在被 lambda 部分替代,但在某些场景下依然好用。比如你需要「提前绑定参数」,或者「适配不同接口」的时候。

我的建议: 能用 lambda 就用 lambda,可读性更好。std::bind 适合那些「需要延迟绑定」或者「参数占位符较多」的场景。但别滥用,bind 嵌套多了,代码就像天书。

29.5 知识体系总览

下面这张图,是我梳理的「C++ 新特性与设计模式」的映射关系。你可以把它当作一个快速参考。

C++ 新特性与设计模式映射关系 智能指针 + RAII 内存安全 Lambda 表达式 行为参数化 移动语义 零拷贝传递 std::function/bind 万能适配器 工厂模式 组合模式 观察者模式 享元模式 策略模式 命令模式 模板方法模式 访问者模式 工厂模式 原型模式 构建器模式 迭代器模式 观察者模式 策略模式 命令模式 责任链模式 核心思想:用语言特性替代设计模式中的「样板代码」 智能指针 → 替代手动内存管理 | Lambda → 替代策略类 | 移动语义 → 替代拷贝 | function → 替代接口

29.6 避坑指南与最佳实践

说了这么多好处,也得聊聊坑。毕竟我在这些坑里摔过不少次。

我曾经踩过的坑:

  • shared_ptr 循环引用: 两个对象互相持有 shared_ptr,导致内存永远释放不了。解决方案:其中一个改成 weak_ptr。
  • lambda 默认按值捕获大对象: 捕获了一个 10MB 的 vector,lambda 拷贝了一次,性能直接崩了。解决方案:按引用捕获,或者用移动语义。
  • std::function 的性能开销: 频繁调用的热路径上,std::function 的虚函数调用开销不可忽视。解决方案:热路径用模板或函数指针,std::function 留给「配置」场景。
  • 移动后对象的状态: 移动后的对象处于「有效但未指定」的状态。别假设它为空,也别继续使用它的值。

我的最佳实践清单:

  1. 工厂模式统一返回 unique_ptr,明确所有权转移
  2. 观察者模式用 weak_ptr 存储观察者,避免循环引用
  3. 策略模式优先用 lambda + std::function,而不是定义接口
  4. 命令模式用 std::function 包装,支持 undo 时额外保存状态
  5. 原型模式提供 clone() 和 move() 两个版本,让调用方选择
  6. 构建器模式返回对象时,用移动语义避免拷贝

29.7 小结

C++11/14/17 的新特性,不是让你抛弃设计模式,而是让你用更现代、更安全的方式去实现它们。智能指针解决了内存问题,lambda 解决了代码冗余问题,移动语义解决了性能问题,std::function 解决了耦合问题。

说白了,设计模式的「意图」没变,但「实现方式」进化了。你想想看,GoF 那本书是 1994 年写的,那时候 C++ 还没有 STL,更没有智能指针。现在我们有这么多好用的工具,没理由还写那种「原始 C++」风格的设计模式。

嗯,这就是我对新特性与设计模式的理解。希望对你有所帮助。


公众号:蓝海资料掘金营,微信deep3321