STL中的设计模式(二)

STL中的设计模式(二)

1. 观察者模式

核心思想:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

STL/现代 C++ 中的体现 :C++ 标准库并没有提供原生的信号/槽机制,但现代 C++ 实现观察者模式的标准惯用法是使用 std::vector<std::function> 来管理订阅者列表。此外,C++20 引入的 std::stop_sourcestd::stop_token 也是观察者模式的一种具体应用(请求取消的通知机制)。

代码示例(基于 STL 组件的信号槽)

C++ 复制代码
#include <iostream>
#include <vector>
#include <functional>
#include <string>

// 利用 STL 组件构建的通用信号(发布者)
template<typename... Args>
class Signal {
public:
    // 使用 std::function 封装各种可调用对象(观察者)
    using SlotType = std::function<void(Args...)>;

    // 订阅(注册观察者)
    void connect(SlotType slot) {
        slots_.push_back(std::move(slot));
    }

    // 发布通知
    void emit(Args... args) {
        for (auto& slot : slots_) {
            slot(args...); // 通知所有观察者
        }
    }

private:
    std::vector<SlotType> slots_; // 观察者列表
};

int main() {
    Signal<std::string, int> newsSignal;

    // 观察者 1:Lambda
    newsSignal.connect([](const std::string& title, int priority) {
        std::cout << "[Subscriber 1] Breaking: " << title << " (P:" << priority << ")\n";
    });

    // 观察者 2:带捕获的 Lambda
    std::string name = "Alice";
    newsSignal.connect([&name](const std::string& title, int priority) {
        std::cout << "[Subscriber 2] " << name << " notified about: " << title << "\n";
    });

    // 触发事件
    newsSignal.emit("C++23 Released!", 1);

    return 0;
}

2. 组合模式

核心思想:将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

STL/现代 C++ 中的体现 :传统组合模式依赖统一的虚基类接口。而在 C++17 后,std::variant 结合递归数据结构,提供了一种类型安全、无需虚函数的全新组合模式实现方式。这在构建 AST(抽象语法树)或 JSON 树时极为常见。

代码示例(基于 std::variant 的静态组合模式)

c++ 复制代码
#include <iostream>
#include <vector>
#include <variant>
#include <memory>

// 前向声明
struct CompositeNode;

// 叶子节点:只包含值
struct LeafNode {
    int value;
};

// 组合节点:可以包含叶子或别的组合节点(树形结构)
struct CompositeNode {
    std::string name;
    // 核心:使用 variant 实现递归的树形结构,统一了叶子与组合的存储
    std::vector<std::variant<LeafNode, std::unique_ptr<CompositeNode>>> children;
};

// 统一的操作接口(访问者/遍历器)
void printTree(const std::variant<LeafNode, std::unique_ptr<CompositeNode>>& node, int indent = 0) {
    std::string prefix(indent, ' ');
    
    // std::visit 实现对统一接口的分发
    std::visit([&](auto&& arg) {
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, LeafNode>) {
            // 对叶子节点的操作
            std::cout << prefix << "- Leaf: " << arg.value << "\n";
        } else if constexpr (std::is_same_v<T, std::unique_ptr<CompositeNode>>) {
            // 对组合节点的操作(递归),对外表现为与叶子同级的接口
            std::cout << prefix << "+ Composite: " << arg->name << "\n";
            for (const auto& child : arg->children) {
                printTree(child, indent + 4);
            }
        }
    }, node);
}

int main() {
    auto root = std::make_unique<CompositeNode>();
    root->name = "Root";
    root->children.push_back(LeafNode{10});
    root->children.push_back(LeafNode{20});
    
    auto subDir = std::make_unique<CompositeNode>();
    subDir->name = "Sub";
    subDir->children.push_back(LeafNode{30});
    
    root->children.push_back(std::move(subDir));

    std::variant<LeafNode, std::unique_ptr<CompositeNode>> rootVariant = std::move(root);
    printTree(rootVariant);

    return 0;
}

3. 享元模式

核心思想:运用共享技术有效地支持大量细粒度的对象,避免拥有相同内容的多余对象造成的内存开销。

STL/现代 C++ 中的体现 :标准库没有直接的享元工厂,但利用 std::unordered_mapstd::weak_ptr / std::shared_ptr 是实现缓存型享元模式的标准 C++ 惯用法。std::weak_ptr 允许在对象无人使用时被自动销毁,完美解决了享元对象的生命周期管理问题。

代码示例(标准库实现的享元工厂)

C++ 复制代码
#include <iostream>
#include <string>
#include <unordered_map>
#include <memory>

// 享元对象(假设非常耗费内存,如大纹理/字体数据)
class HeavyFont {
public:
    HeavyFont(const std::string& name) : name_(name) {
        std::cout << "Loading heavy font: " << name_ << "\n"; // 模拟耗时加载
    }
    void render(const std::string& text) { std::cout << "Rendering [" << text << "] with " << name_ << "\n"; }
private:
    std::string name_;
};

// 享元工厂
class FontFactory {
public:
    using FontPtr = std::shared_ptr<HeavyFont>;

    FontPtr getFont(const std::string& name) {
        // 检查缓存是否过期(weak_ptr 的妙用)
        auto it = cache_.find(name);
        if (it != cache_.end()) {
            if (auto ptr = it->second.lock()) {
                std::cout << "Cache hit: " << name << "\n";
                return ptr; // 缓存命中,直接共享
            }
        }
        
        // 缓存未命中或已过期,创建新对象
        std::cout << "Cache miss: " << name << "\n";
        FontPtr newFont = std::make_shared<HeavyFont>(name);
        cache_[name] = newFont; // weak_ptr 赋值
        return newFont;
    }
private:
    std::unordered_map<std::string, std::weak_ptr<HeavyFont>> cache_; // 享元池
};

int main() {
    FontFactory factory;
    
    auto font1 = factory.getFont("Arial");   // 加载
    auto font2 = factory.getFont("Arial");   // 共享
    font1->render("Hello");
    
    font1.reset(); // 释放 font1
    // 此时 Arial 的内存并没有被销毁,因为 font2 还在引用
    
    auto font3 = factory.getFont("Arial");   // 依然共享
    font2.reset(); 
    font3.reset(); // 真正释放 Arial 内存
    
    auto font4 = factory.getFont("Arial");   // 重新加载(享元被回收了)
    return 0;
}

4. 装饰器模式

核心思想:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。

STL/现代 C++ 中的体现 :C++20 引入的 Ranges 库(std::views 是装饰器模式的巅峰之作。views::filterviews::transform 就像一层层装饰器,动态地给底层容器叠加过滤、转换逻辑,而不会拷贝底层数据,且支持延迟计算。

代码示例(C++20 Ranges 视图装饰)

C++ 复制代码
#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 原始数据像是一个核心对象
    auto view = data 
        | std::views::filter([](int n) { return n % 2 == 0; })   // 装饰器1:只保留偶数
        | std::views::transform([](int n) { return n * 2; });    // 装饰器2:数值翻倍

    // 此时 view 并没有真正执行计算(惰性求值),它只是被"装饰"了
    // 当我们遍历时,装饰逻辑才生效
    for (int val : view) {
        std::cout << val << " "; // 输出 4 8 12 16 20
    }
    std::cout << "\n";

    return 0;
}

5. 桥接模式

核心思想:将抽象部分与它的实现部分分离,使它们都可以独立地变化。

STL/现代 C++ 中的体现 :经典案例是 std::iostream 家族与 std::streambufiostream 是抽象接口(输入/输出操作),而 streambuf 是底层实现(具体的读写字节源:文件、字符串、控制台)。你可以将任意 streambuf 桥接到 iostream 上,改变底层行为而无需改变接口。

代码示例(流与缓冲区的桥接)

C++ 复制代码
#include <iostream>
#include <sstream>
#include <fstream>

void processInput(std::istream& stream) {
    // 抽象接口:只关心从流中读取,不关心数据来自哪里
    std::string word;
    while (stream >> word) {
        std::cout << "Read: " << word << "\n";
    }
}

int main() {
    // 实现A:从字符串读取
    std::istringstream iss("Hello from string");
    std::cout << "--- From String ---\n";
    processInput(iss);

    // 实现B:从文件读取(假设有 data.txt)
    // std::ifstream ifs("data.txt"); 
    // processInput(ifs);

    // 桥接的动态性:运行时替换实现
    std::ostringstream oss;
    // 将 ooss 的底层缓冲区桥接到 cout 上,导致所有输出重定向
    auto oldBuf = std::cout.rdbuf(oss.rdbuf()); 
    std::cout << "This goes to string, not console!" << std::endl;
    std::cout.rdbuf(oldBuf); // 恢复

    std::cout << "Captured: " << oss.str() << "\n";

    return 0;
}

6. 访问者模式

核心思想:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

STL/现代 C++ 中的体现 :传统访问者模式需要复杂的双分派和虚函数。现代 C++ 使用 std::variant + std::visit 实现了编译时静态访问者(也称为代数数据类型)。这种方式完全消除了虚函数开销,且类型极其安全。

代码示例(std::visit 实现的静态访问者)

c++ 复制代码
#include <iostream>
#include <variant>
#include <vector>

// 被访问的元素定义
struct Circle { double radius; };
struct Square { double side; };
struct Triangle { double base; double height; };

// 使用 variant 定义对象结构
using Shape = std::variant<Circle, Square, Triangle>;

// 访问者:利用 C++17 的 overloaded 惯用法(推导指引)
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

int main() {
    std::vector<Shape> shapes = {
        Circle{5.0},
        Square{4.0},
        Triangle{6.0, 3.0}
    };

    // 新操作1:计算面积(无需修改 Circle/Square 等结构体定义)
    auto areaVisitor = overloaded {
        [](const Circle& c) { return 3.14159 * c.radius * c.radius; },
        [](const Square& s) { return s.side * s.side; },
        [](const Triangle& t) { return 0.5 * t.base * t.height; }
    };

    // 新操作2:绘制描述(同样无需修改结构体)
    auto drawVisitor = overloaded {
        [](const Circle& c) { std::cout << "Drawing Circle\n"; },
        [](const Square& s) { std::cout << "Drawing Square\n"; },
        [](const Triangle& t) { std::cout << "Drawing Triangle\n"; }
    };

    for (const auto& shape : shapes) {
        std::cout << "Area: " << std::visit(areaVisitor, shape) << "\n";
        std::visit(drawVisitor, shape);
    }

    return 0;
}

7. 责任链模式

核心思想:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。

STL/现代 C++ 中的体现 :C++ 标准库没有直接提供责任链,但基于 std::vector<std::function>std::forward_list 构建中间件/拦截器链是标准 C++ 网络库(如 Boost.Beast 非官方设计)或日志框架的常见惯用法。

代码示例(基于 STL 容器的函数链)

C++ 复制代码
#include <iostream>
#include <vector>
#include <functional>
#include <string>

// 模拟 HTTP 请求
struct Request {
    std::string url;
    bool isHandled = false;
};

// 责任链节点:返回 true 表示请求已处理,终止传递;返回 false 传递给下一个
using Handler = std::function<bool(Request&)>;

class MiddlewareChain {
public:
    void addHandler(Handler handler) {
        handlers_.push_back(std::move(handler));
    }

    void process(Request& req) {
        for (auto& handler : handlers_) {
            if (handler(req)) {
                std::cout << "[Chain] Request handled, stopping propagation.\n";
                break; // 责任链终止
            }
        }
        if (!req.isHandled) {
            std::cout << "[Chain] Request reached the end unhandled.\n";
        }
    }

private:
    std::vector<Handler> handlers_;
};

int main() {
    MiddlewareChain chain;

    // 处理者 1:鉴权中间件
    chain.addHandler([](Request& req) {
        if (req.url.find("/admin") != std::string::npos) {
            std::cout << "[Auth] Admin area detected. Unauthorized!\n";
            req.isHandled = true;
            return true; // 拦截,不继续传递
        }
        std::cout << "[Auth] Public area. Passing...\n";
        return false;
    });

    // 处理者 2:日志中间件
    chain.addHandler([](Request& req) {
        std::cout << "[Logger] Logging request to: " << req.url << "\n";
        return false; // 记录日志后总是放行
    });

    // 处理者 3:业务处理
    chain.addHandler([](Request& req) {
        std::cout << "[Business] Handling business logic.\n";
        req.isHandled = true;
        return true;
    });

    // 测试 1:普通请求,贯穿 1 -> 2 -> 3
    Request req1{"/home", false};
    chain.process(req1);
    std::cout << "-------------------\n";

    // 测试 2:受限请求,在 1 处被拦截
    Request req2{"/admin/panel", false};
    chain.process(req2);

    return 0;
}
相关推荐
沈阳信息学奥赛培训2 小时前
C++ 位运算练习题
开发语言·c++
小燚~2 小时前
MSVCR100.dII报错问题处理
c++·windows·qt
Oj92q85H52 小时前
如何在Dev-C++中使用TDM-GCC编译多个文件
开发语言·c++
wengqidaifeng2 小时前
C++从菜鸟到强手:2.类和对象(下)—— 进阶特性与完整日期类实现
开发语言·c++
Oj92q85H53 小时前
如何在Dev-C++中设置TDM-GCC编译器
开发语言·c++
悟05153 小时前
设计模式-模板模式
设计模式
BLSxiaopanlaile3 小时前
有关创建型的几个设计模式总结
设计模式
学无止境_永不停歇3 小时前
从零手写高性能 C++ TCP 服务器框架(一):项目介绍
linux·服务器·c++·中间件
并不喜欢吃鱼3 小时前
从零开始 C++----十【C++ 数据结构】AVL 树详解:从原理到实现
开发语言·数据结构·c++