设计模式:单例模式、工厂模式、观察者模式、策略模式在C++中的高性能实现

设计模式这东西,很多人觉得是Java的专属。其实在C++里,用好了设计模式,性能反而能上一个台阶。我这些年做高性能服务端,几乎每个项目都离不开这四种模式。今天咱们就聊聊,怎么在C++里把它们写出高性能的味道。

单例模式:别再用双重检查锁了

单例模式是最常用的,也是最容易被写烂的。很多人一上来就写双重检查锁(DCLP),觉得这样既线程安全又高效。嗯,这里要注意——在C++里,DCLP其实是个坑。

注意: 双重检查锁在C++11之前存在内存序问题,即使C++11之后,也需要配合std::atomic和内存屏障,稍不注意就出bug。

我个人习惯用Meyer's Singleton,也就是函数局部静态变量。C++11标准保证了局部静态变量的初始化是线程安全的,而且性能极好。

// 推荐:Meyer's Singleton
class Logger {
public:
    static Logger& instance() {
        static Logger inst;
        return inst;
    }
    
    void log(const std::string& msg) {
        // 实际写日志逻辑
    }
    
private:
    Logger() = default;
    ~Logger() = default;
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
};

我在项目中遇到过一个问题:某个高频交易系统里,日志单例被每秒调用几十万次。用Meyer's Singleton,每次调用只是检查一个标志位,开销几乎为零。而如果用DCLP,每次都要走一遍原子变量的读操作,性能差了一个数量级。

小技巧: 如果你的单例对象很大,或者构造时有IO操作,可以考虑延迟初始化。但大多数情况下,Meyer's Singleton已经够用了。

工厂模式:用模板代替虚函数

工厂模式的核心是“创建对象但不暴露创建逻辑”。传统做法是用虚函数+继承,但虚函数调用有开销,而且每次创建对象都要走一次虚表。在高性能场景下,这不可接受。

我建议用模板工厂,把类型信息编译期就确定下来。说白了,就是用模板参数代替运行时多态。

// 高性能模板工厂
template<typename Base, typename... Args>
class Factory {
public:
    template<typename Derived>
    static void registerType(const std::string& name) {
        creators()[name] = [](Args... args) -> std::unique_ptr<Base> {
            return std::make_unique<Derived>(std::forward<Args>(args)...);
        };
    }
    
    static std::unique_ptr<Base> create(const std::string& name, Args... args) {
        auto it = creators().find(name);
        if (it != creators().end()) {
            return it->second(std::forward<Args>(args)...);
        }
        return nullptr;
    }
    
private:
    static std::unordered_map<std::string, 
        std::function<std::unique_ptr<Base>(Args...)>>& creators() {
        static std::unordered_map<std::string, 
            std::function<std::unique_ptr<Base>(Args...)>> map;
        return map;
    }
};

// 使用示例
class Shape { public: virtual void draw() = 0; };
class Circle : public Shape { 
public: 
    void draw() override { /* ... */ } 
};

// 注册
Factory<Shape>::registerType<Circle>("circle");

// 创建
auto shape = Factory<Shape>::create("circle");

你想想看,这个工厂在编译期就把所有类型信息都处理好了。运行时只需要一次哈希查找,没有虚函数调用。我在一个游戏引擎里用过这个方案,创建上万个对象,性能比传统工厂快了3倍以上。

核心要点: 工厂模式的高性能实现,关键在于把运行时多态尽量转化为编译期多态。模板是你的好朋友。

观察者模式:用信号槽代替回调列表

观察者模式,说白了就是“发布-订阅”。传统做法是维护一个回调函数列表,事件发生时挨个调用。但这样做有两个问题:一是遍历列表有性能开销,二是回调函数的生命周期管理容易出问题。

我推荐用信号槽(Signal-Slot)机制。Qt的信号槽很出名,但太重了。我们可以自己实现一个轻量级的版本,用模板和变参模板来优化。

// 轻量级信号槽
template<typename... Args>
class Signal {
public:
    using Slot = std::function<void(Args...)>;
    
    // 连接槽函数,返回连接ID用于断开
    size_t connect(Slot slot) {
        slots_.emplace_back(nextId_, std::move(slot));
        return nextId_++;
    }
    
    // 断开连接
    void disconnect(size_t id) {
        slots_.erase(
            std::remove_if(slots_.begin(), slots_.end(),
                [id](const auto& pair) { return pair.first == id; }),
            slots_.end()
        );
    }
    
    // 发射信号
    void emit(Args... args) {
        for (auto& [id, slot] : slots_) {
            slot(std::forward<Args>(args)...);
        }
    }
    
private:
    std::vector<std::pair<size_t, Slot>> slots_;
    size_t nextId_ = 0;
};

// 使用示例
Signal<int, std::string> onDataReceived;

onDataReceived.connect([](int id, const std::string& data) {
    std::cout << "Received: " << id << " - " << data << std::endl;
});

onDataReceived.emit(42, "Hello");

我曾经在一个网络库里用这个方案。每次收到网络包,只需要一次emit调用,所有订阅者都能收到通知。而且因为用的是vector而不是list,缓存局部性很好,遍历速度极快。

注意: 如果槽函数执行时间较长,建议用异步方式处理。否则一个慢槽会阻塞整个emit调用。

策略模式:用函数指针代替虚函数

策略模式的核心是“把算法封装成可互换的策略”。传统做法是定义策略接口,然后用继承实现不同策略。但虚函数调用有开销,而且每个策略都要写一个类,太啰嗦。

我建议用函数指针或std::function来实现策略。这样既灵活又高效,而且代码量少很多。

// 策略模式:用函数指针实现
class SortAlgorithm {
public:
    using SortFunc = void(*)(std::vector<int>&);
    
    void setStrategy(SortFunc func) {
        strategy_ = func;
    }
    
    void execute(std::vector<int>& data) {
        if (strategy_) {
            strategy_(data);
        }
    }
    
private:
    SortFunc strategy_ = nullptr;
};

// 具体策略
void quickSort(std::vector<int>& data) {
    std::sort(data.begin(), data.end());
}

void bubbleSort(std::vector<int>& data) {
    // 冒泡排序实现
}

// 使用
SortAlgorithm sorter;
sorter.setStrategy(quickSort);
sorter.execute(myData);

你想想看,函数指针的调用开销几乎为零。而且策略之间切换只需要赋值一个指针,比创建新的策略对象快多了。我在一个实时渲染引擎里用过这个方案,根据场景动态切换渲染策略,性能表现非常好。

提示: 如果策略需要捕获上下文,可以用std::function代替函数指针。虽然std::function有轻微开销,但比虚函数还是快很多。

四种模式对比

模式 传统实现 高性能实现 适用场景
单例模式 双重检查锁 Meyer's Singleton 全局唯一对象,如日志、配置
工厂模式 虚函数+继承 模板工厂 大量对象创建,类型在编译期已知
观察者模式 回调列表 信号槽 事件驱动系统,如网络库、UI
策略模式 策略接口+继承 函数指针/std::function 算法切换,如排序、渲染

知识体系图

C++设计模式高性能实现 单例模式 Meyer's Singleton 工厂模式 模板工厂 观察者模式 信号槽 策略模式 函数指针 核心原则 编译期多态 > 运行时多态 性能关键点 减少虚函数调用 · 利用模板 · 优化缓存局部性 · 避免锁竞争

这四种模式,说白了就是C++高性能编程的四大金刚。每个模式都有它的适用场景,也有它的高性能实现方式。我在实际项目中,几乎每天都在用这些技巧。记住一点:性能优化不是靠奇技淫巧,而是靠对语言特性的深刻理解

总结: 单例用Meyer's Singleton,工厂用模板,观察者用信号槽,策略用函数指针。这四招用好了,你的C++代码性能至少提升一个档次。

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