25、STL与设计模式:迭代器模式、适配器模式、策略模式、观察者模式

说实话,很多C++程序员学了STL却不会用设计模式,或者学了设计模式却不知道怎么跟STL结合。其实这两者天生就是一对。STL本身就是设计模式的集大成者——迭代器模式、适配器模式、策略模式,这些在STL里随处可见。

我当年刚接触STL时,只觉得它是一堆容器和算法的集合。后来做项目多了才明白,STL的设计思想就是设计模式的最佳实践。今天咱们就来聊聊STL里藏着的那些设计模式,以及怎么用它们搭建一个事件驱动系统。

核心观点:STL不是工具库,它是设计模式的教科书。理解这些模式,你才能真正驾驭STL。

25.1 迭代器模式:STL的骨架

迭代器模式,说白了就是提供一种统一的方式来遍历容器,而不暴露容器的内部结构。STL里的迭代器就是最经典的实现。

我习惯把迭代器比作「智能指针」。它知道怎么往前走、怎么取值、怎么比较。不同的容器有不同的迭代器,但用法是一样的。

// 迭代器模式的核心:统一遍历接口
std::vector<int> vec = {1, 2, 3, 4, 5};
std::list<int> lst = {10, 20, 30, 40, 50};

// 同样的写法,遍历不同的容器
auto print = [](const auto& container) {
    for (auto it = container.begin(); it != container.end(); ++it) {
        std::cout << *it << " ";
    }
};

print(vec);  // 输出: 1 2 3 4 5
print(lst);  // 输出: 10 20 30 40 50

你看,迭代器把容器和算法解耦了。算法只需要知道迭代器的接口,不需要关心底层是数组还是链表。这就是迭代器模式的价值。

我的经验:写泛型算法时,尽量用迭代器而不是直接操作容器。这样你的算法就能适配任何容器。我曾经写过一个排序工具,一开始只支持vector,后来改成迭代器版本,直接就能用在deque和list上。

25.2 适配器模式:stack/queue的底层秘密

适配器模式,就是把一个接口转换成另一个接口。STL里的stack和queue就是典型的适配器——它们底层用的是deque,但对外暴露的是栈和队列的接口。

为什么会这样?因为deque已经提供了所有需要的操作:push_back、pop_back、push_front、pop_front。stack只需要把deque的接口「包装」一下,只暴露push/pop/top,就变成了栈。

// stack的底层实现(简化版)
template <typename T, typename Container = std::deque<T>>
class stack {
public:
    void push(const T& value) { c.push_back(value); }
    void pop() { c.pop_back(); }
    T& top() { return c.back(); }
    bool empty() const { return c.empty(); }
    
private:
    Container c;  // 默认用deque
};

queue也是一样的道理。它把deque的push_back和pop_front包装成push和pop。这就是适配器模式——不改变底层实现,只改变接口。

注意:stack和queue默认用deque,但你可以换成vector或list。不过要注意,vector没有pop_front,所以不能做queue的底层容器。我踩过这个坑,当时想用vector做queue的底层,编译直接报错。

25.3 策略模式:函数对象与lambda

策略模式,就是把算法封装成对象,让算法可以互相替换。STL里的函数对象(functor)和lambda就是策略模式的体现。

你想想看,std::sort的第三个参数就是一个策略——你想怎么排序?升序、降序、还是按某个字段排序?这个策略可以随时换。

// 策略模式:不同的排序策略
std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6};

// 策略1:升序
std::sort(data.begin(), data.end(), std::less<int>());

// 策略2:降序
std::sort(data.begin(), data.end(), std::greater<int>());

// 策略3:自定义策略(按绝对值排序)
std::sort(data.begin(), data.end(), 
    [](int a, int b) { return std::abs(a) < std::abs(b); });

我建议你在项目中多用函数对象而不是普通函数。为什么?因为函数对象可以保存状态,可以内联优化,性能更好。lambda就是函数对象的语法糖,本质上是一样的。

实战技巧:写一个可配置的排序工具,把比较策略作为模板参数。这样编译期就能确定策略,没有虚函数开销。

25.4 观察者模式:signal/slot模拟

观察者模式,就是当一个对象状态变化时,自动通知所有依赖它的对象。STL本身没有直接提供观察者模式,但我们可以用std::function和std::vector来模拟。

我记得在一个UI框架项目中,需要实现按钮点击事件。当时没有用Qt的signal/slot,而是自己用STL搭了一个轻量级的观察者系统。

// 用STL模拟观察者模式
class EventEmitter {
public:
    // 注册观察者
    template <typename Func>
    void connect(Func&& f) {
        slots_.push_back(std::forward<Func>(f));
    }
    
    // 触发事件
    void emit(int data) {
        for (auto& slot : slots_) {
            slot(data);
        }
    }
    
private:
    std::vector<std::function<void(int)>> slots_;
};

// 使用
EventEmitter emitter;
emitter.connect([](int x) { std::cout << "观察者1: " << x << "\n"; });
emitter.connect([](int x) { std::cout << "观察者2: " << x * 2 << "\n"; });

emitter.emit(42);  // 两个观察者都会被通知
我的建议:生产环境中别自己造轮子,用Boost.Signals2或者Qt的信号槽。但理解原理很重要——说白了就是std::function的集合。

25.5 实战:事件驱动系统

好了,前面讲了这么多模式,咱们来实战一把。用STL搭建一个简单的事件驱动系统,把迭代器、适配器、策略、观察者都用上。

这个系统的核心思想是:事件源产生事件,事件经过管道(适配器)处理,最终分发给观察者。不同的处理策略可以随时切换。

// 事件驱动系统框架
class EventSystem {
public:
    using EventHandler = std::function<void(const std::string&)>;
    
    // 注册事件处理器(观察者模式)
    void on(const std::string& event, EventHandler handler) {
        handlers_[event].push_back(handler);
    }
    
    // 触发事件
    void emit(const std::string& event, const std::string& data) {
        auto it = handlers_.find(event);
        if (it != handlers_.end()) {
            for (auto& handler : it->second) {
                handler(data);
            }
        }
    }
    
    // 事件管道(适配器模式)
    template <typename Func>
    void pipe(const std::string& event, Func transform) {
        auto old_handler = handlers_[event].back();
        handlers_[event].back() = [transform, old_handler](const std::string& data) {
            old_handler(transform(data));
        };
    }
    
private:
    std::unordered_map<std::string, std::vector<EventHandler>> handlers_;
};

// 使用示例
EventSystem sys;

// 注册事件
sys.on("click", [](const std::string& msg) {
    std::cout << "点击事件: " << msg << "\n";
});

// 添加管道:把消息转成大写(策略模式)
sys.pipe("click", [](const std::string& s) {
    std::string result = s;
    std::transform(result.begin(), result.end(), result.begin(), ::toupper);
    return result;
});

// 触发事件
sys.emit("click", "hello world");  // 输出: 点击事件: HELLO WORLD

这个系统虽然简单,但已经包含了观察者模式(事件分发)、适配器模式(管道转换)、策略模式(可替换的转换函数)。迭代器模式则体现在std::transform和容器的遍历上。

避坑指南:我曾经在事件系统中用过裸指针做观察者,结果对象销毁后忘记移除,导致野指针崩溃。后来改用std::shared_ptr和weak_ptr的组合,或者用RAII管理连接生命周期。记住:观察者模式最大的坑就是生命周期管理。

25.6 知识体系总览

下面这张图把本章的知识点串起来了。你可以看到STL设计模式之间的关联,以及它们如何共同构成事件驱动系统。

STL与设计模式知识体系 STL设计模式 迭代器模式 适配器模式 策略模式 观察者模式 统一遍历接口 stack/queue包装 函数对象/lambda signal/slot模拟 实战:事件驱动系统

这张图展示了四个设计模式如何独立存在,又如何在事件驱动系统中协同工作。迭代器负责数据遍历,适配器负责接口转换,策略负责算法替换,观察者负责事件通知。把它们组合起来,就是一个灵活可扩展的系统。

总结:STL不是死记硬背的API,它是设计模式的活教材。理解背后的模式,你就能写出更优雅、更可维护的C++代码。下次用STL时,多想想:这个容器用了什么模式?这个算法体现了什么思想?久而久之,你的代码水平会上一个台阶。

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