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++ 新特性与设计模式」的映射关系。你可以把它当作一个快速参考。
29.6 避坑指南与最佳实践
说了这么多好处,也得聊聊坑。毕竟我在这些坑里摔过不少次。
我曾经踩过的坑:
- shared_ptr 循环引用: 两个对象互相持有 shared_ptr,导致内存永远释放不了。解决方案:其中一个改成 weak_ptr。
- lambda 默认按值捕获大对象: 捕获了一个 10MB 的 vector,lambda 拷贝了一次,性能直接崩了。解决方案:按引用捕获,或者用移动语义。
- std::function 的性能开销: 频繁调用的热路径上,std::function 的虚函数调用开销不可忽视。解决方案:热路径用模板或函数指针,std::function 留给「配置」场景。
- 移动后对象的状态: 移动后的对象处于「有效但未指定」的状态。别假设它为空,也别继续使用它的值。
我的最佳实践清单:
- 工厂模式统一返回 unique_ptr,明确所有权转移
- 观察者模式用 weak_ptr 存储观察者,避免循环引用
- 策略模式优先用 lambda + std::function,而不是定义接口
- 命令模式用 std::function 包装,支持 undo 时额外保存状态
- 原型模式提供 clone() 和 move() 两个版本,让调用方选择
- 构建器模式返回对象时,用移动语义避免拷贝
29.7 小结
C++11/14/17 的新特性,不是让你抛弃设计模式,而是让你用更现代、更安全的方式去实现它们。智能指针解决了内存问题,lambda 解决了代码冗余问题,移动语义解决了性能问题,std::function 解决了耦合问题。
说白了,设计模式的「意图」没变,但「实现方式」进化了。你想想看,GoF 那本书是 1994 年写的,那时候 C++ 还没有 STL,更没有智能指针。现在我们有这么多好用的工具,没理由还写那种「原始 C++」风格的设计模式。
嗯,这就是我对新特性与设计模式的理解。希望对你有所帮助。
公众号:蓝海资料掘金营,微信deep3321