28. 设计模式与C++11/14/17新特性:lambda表达式简化策略模式,智能指针与RAII
说实话,很多人在学设计模式时,总觉得那是Java的专利。其实C++玩起设计模式来,反而更灵活。尤其是C++11之后,lambda表达式、智能指针这些东西一出来,很多经典模式的实现方式都变了。
我个人习惯把这种变化叫做「现代C++对设计模式的降维打击」。今天我们就聊聊两个最典型的例子:lambda怎么让策略模式变得更轻量,智能指针怎么让RAII变得更安全。
一、传统策略模式的痛点
先看一个经典场景。你写了一个排序算法,想支持不同的比较策略。传统做法是这样的:
// 传统策略模式
class SortStrategy {
public:
virtual ~SortStrategy() = default;
virtual bool compare(int a, int b) = 0;
};
class AscendingSort : public SortStrategy {
public:
bool compare(int a, int b) override {
return a < b;
}
};
class DescendingSort : public SortStrategy {
public:
bool compare(int a, int b) override {
return a > b;
}
};
class Sorter {
std::unique_ptr<SortStrategy> strategy_;
public:
explicit Sorter(std::unique_ptr<SortStrategy> strategy)
: strategy_(std::move(strategy)) {}
void sort(std::vector<int>& data) {
std::sort(data.begin(), data.end(),
[this](int a, int b) {
return strategy_->compare(a, b);
});
}
};
你发现问题了吗?为了一个简单的比较逻辑,你写了三个类。每个类就一行核心代码。我在项目中见过更夸张的,一个策略接口派生十几个子类,每个子类就重写一个方法。维护起来简直要命。
我曾经踩过的坑:有一次接手一个遗留系统,里面策略模式用了二十多个子类。每次新增策略都要新建一个.h和一个.cpp文件。后来我重构时发现,大部分策略逻辑其实就是一行lambda的事。
二、lambda表达式让策略模式脱胎换骨
C++11的lambda表达式,说白了就是一个匿名函数对象。它天然适合做策略。你想想看,策略模式的核心是什么?就是把算法封装起来,让它们可以互相替换。lambda正好满足这个需求。
用lambda重写上面的例子:
// 现代C++策略模式
using CompareFunc = std::function<bool(int, int)>;
class Sorter {
CompareFunc compare_;
public:
explicit Sorter(CompareFunc compare)
: compare_(std::move(compare)) {}
void sort(std::vector<int>& data) {
std::sort(data.begin(), data.end(), compare_);
}
};
// 使用方式
int main() {
std::vector<int> data = {3, 1, 4, 1, 5, 9};
// 升序策略 - 直接传lambda
Sorter ascSorter([](int a, int b) { return a < b; });
ascSorter.sort(data);
// 降序策略 - 换个lambda
Sorter descSorter([](int a, int b) { return a > b; });
descSorter.sort(data);
// 甚至可以在运行时动态决定
bool ascending = true;
Sorter dynamicSorter([ascending](int a, int b) {
return ascending ? a < b : a > b;
});
}
看到了吗?不需要继承,不需要虚函数,不需要新建类。一个lambda就搞定。代码量减少了一半以上,可读性反而提升了。
核心要点:lambda + std::function 的组合,可以替代大部分传统的策略模式实现。当策略逻辑简单时,优先考虑lambda。
三、智能指针与RAII的进化
RAII(资源获取即初始化)是C++最核心的设计思想之一。但传统RAII有个问题:资源的所有权不清晰。你写了一个类管理文件句柄,别人拷贝了你的对象,然后两个对象同时去close同一个文件——崩溃。
C++11引入的智能指针,完美解决了这个问题。我习惯把它们分成三类:
| 智能指针 | 所有权模型 | 适用场景 |
|---|---|---|
| std::unique_ptr | 独占所有权 | 资源唯一拥有者,如工厂方法返回值 |
| std::shared_ptr | 共享所有权 | 多个对象需要共同管理资源,如观察者模式 |
| std::weak_ptr | 弱引用 | 打破循环引用,缓存场景 |
来看一个实际例子。假设你要实现一个数据库连接池:
class DatabaseConnection {
// 模拟数据库连接
public:
void query(const std::string& sql) {
std::cout << "Executing: " << sql << std::endl;
}
};
class ConnectionPool {
std::vector<std::unique_ptr<DatabaseConnection>> connections_;
public:
std::unique_ptr<DatabaseConnection> acquire() {
if (connections_.empty()) {
return std::make_unique<DatabaseConnection>();
}
auto conn = std::move(connections_.back());
connections_.pop_back();
return conn;
}
void release(std::unique_ptr<DatabaseConnection> conn) {
connections_.push_back(std::move(conn));
}
};
这里用unique_ptr管理连接,语义非常清晰:连接池拥有连接的所有权,客户端借走连接后,所有权转移给客户端。客户端用完归还,所有权又回到池子。整个过程不需要手动delete,不会内存泄漏。
我的建议:能用unique_ptr就别用shared_ptr。shared_ptr有引用计数的性能开销,而且容易搞出循环引用。我见过一个项目,全部用shared_ptr,结果内存占用比预期高了30%。
四、RAII + 智能指针的最佳实践
把RAII和智能指针结合起来,可以写出非常优雅的代码。我总结了几条原则:
- 资源封装成类:构造函数获取资源,析构函数释放资源
- 使用智能指针管理资源对象:而不是直接管理资源本身
- 明确所有权语义:独占用unique_ptr,共享用shared_ptr
- 避免裸new/delete:用std::make_unique和std::make_shared
举个例子,一个文件操作的RAII封装:
class File {
FILE* fp_;
public:
explicit File(const char* filename, const char* mode)
: fp_(fopen(filename, mode)) {
if (!fp_) {
throw std::runtime_error("Failed to open file");
}
}
~File() {
if (fp_) {
fclose(fp_);
}
}
// 禁止拷贝,允许移动
File(const File&) = delete;
File& operator=(const File&) = delete;
File(File&& other) noexcept : fp_(other.fp_) {
other.fp_ = nullptr;
}
};
// 使用智能指针管理File对象
auto file = std::make_unique<File>("test.txt", "w");
// 当file离开作用域时,File析构函数自动关闭文件
嗯,这里要注意:RAII类本身已经管理了资源,为什么还要用智能指针?因为智能指针管理的是对象的生命周期。如果你需要把File对象传递给其他函数,或者放入容器中,智能指针就派上用场了。
五、知识体系总览
下面这张图总结了本章的核心内容:
六、避坑指南
最后分享几个我实际踩过的坑:
- lambda捕获陷阱:按引用捕获局部变量时,lambda生命周期不能超过变量生命周期。我曾经在异步回调里用了[&],结果变量已经销毁了,程序直接崩溃。
- shared_ptr循环引用:两个对象互相持有shared_ptr,会导致内存泄漏。用weak_ptr打破循环。我见过一个服务因为这个原因,内存占用每天涨100MB。
- 不要滥用lambda:如果策略逻辑很复杂(超过20行),还是建议封装成函数或类。lambda的优势在于简洁,复杂逻辑会降低可读性。
- make_unique和make_shared优先:直接new智能指针会导致两次内存分配(控制块和对象分开),而make系列函数一次分配搞定。
说白了,C++11/14/17的新特性不是要取代设计模式,而是让设计模式的实现更优雅。lambda让策略模式从「类爆炸」变成了「一行代码」,智能指针让RAII从「手动管理」变成了「自动管理」。这才是现代C++该有的样子。
一句话总结:用lambda简化策略,用智能指针强化RAII。你的代码会更短、更安全、更容易维护。
公众号:蓝海资料掘金营,微信deep3321