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和智能指针结合起来,可以写出非常优雅的代码。我总结了几条原则:

  1. 资源封装成类:构造函数获取资源,析构函数释放资源
  2. 使用智能指针管理资源对象:而不是直接管理资源本身
  3. 明确所有权语义:独占用unique_ptr,共享用shared_ptr
  4. 避免裸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对象传递给其他函数,或者放入容器中,智能指针就派上用场了。

五、知识体系总览

下面这张图总结了本章的核心内容:

C++新特性对设计模式的优化 设计模式 + C++11/14/17 策略模式 传统:虚函数 + 继承 现代:lambda + std::function RAII + 智能指针 unique_ptr:独占所有权 shared_ptr:共享所有权 最佳实践 资源封装成类 + 智能指针管理 核心思想:用语言特性简化模式实现,而不是生搬硬套 lambda消除策略类的冗余,智能指针让RAII更安全 更简洁、更安全、更现代的C++代码

六、避坑指南

最后分享几个我实际踩过的坑:

  • 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