1. 项目概述:从“复制粘贴”到“优雅复用”的思维跃迁

干了十多年C++,我见过太多项目初期跑得飞快,中期步履蹒跚,后期直接推倒重来的案例。核心痛点往往不是算法不够精妙,也不是性能不够极致,而是代码在“复用”和“设计”上栽了大跟头。新人最常问:“这个功能A模块能用,B模块为什么不能用?我复制过来改改行不行?”老手则常陷入沉思:“这个新需求,到底该继承原来的类,还是组合一个新的对象?改动一处,会不会引发十处报错?”

“代码复用”和“设计模式”,这两个词在教科书里总是成对出现,听起来高大上,但在实际C++工程中,它们更像是一对需要精心调和的矛盾体。盲目追求复用,可能导致类与类之间耦合得像一团乱麻,牵一发而动全身;而为了设计模式而设计模式,又会造出一堆过度抽象、难以理解的“范式代码”,让后续维护者望而生畏。

我解决这个难题的核心理念,并非生搬硬套23种设计模式,而是建立一套从问题出发、以 解耦 扩展性 为目标的思维框架。简单来说,就是先识别出代码中那些“变化”的部分和“稳定”的部分,然后用最合适的方式将“变化”封装起来,让“稳定”的部分尽可能不受影响。这个过程,C++的诸多特性(如模板、智能指针、lambda、右值引用等)是我们的利器,而非束缚。

这篇文章,我就结合这些年踩过的坑和总结的经验,聊聊如何在C++项目中,真正落地解决代码复用与设计模式的难题。无论你是正在被祖传代码折磨的工程师,还是希望写出更健壮、更易维护代码的开发者,相信都能从中找到共鸣和可实操的方案。

2. 核心困境解析:为什么你的“复用”总是失败?

在深入解决方案之前,我们必须先诊断清楚问题。C++项目中,代码复用失败和设计模式滥用,通常源于以下几个典型的认知误区和实践陷阱。

2.1 “复用”不等于“复制粘贴”

这是最常见的初级错误。看到一段实现某个功能的代码,直接 Ctrl+C, Ctrl+V 到另一个地方,然后修改变量名、调整几个参数。短期内看似提高了开发效率,但埋下了巨大的隐患:

  1. 逻辑重复 :同一段业务逻辑散落在项目各处。当业务规则变更时,你需要找到所有复制粘贴的地方进行修改,极易遗漏。
  2. bug扩散 :如果原始代码存在隐藏bug,那么这个bug会被复制到多个地方,排查和修复成本呈指数级增长。
  3. 知识碎片化 :后续维护者需要理解多份相似的代码,而不是一份权威的实现,增加了学习成本。

真正的复用,是 逻辑的复用 ,而非代码文本的复制。目标是“一处定义,多处使用”。

2.2 过度设计 vs. 设计不足

这是天平的两端,都可能导致项目失败。

  • 设计不足(Under-engineering) :俗称“写到哪里算哪里”。类职责不清晰,函数长达数百行,全局变量四处飞。初期开发快,但一旦需求变化或需要添加新功能,就会发现无处下手,修改成本极高。这种代码库中几乎谈不上“设计模式”,因为根本没有设计。
  • 过度设计(Over-engineering) :尤其是在学习了设计模式之后,容易犯的毛病。不管三七二十一,先给每个类套上抽象接口,用上工厂模式、装饰器模式、访问者模式……结果为了一个简单的CRUD操作,需要实例化七八个对象,穿越五六层抽象。这极大地增加了代码的复杂度和理解难度,违背了“KISS(Keep It Simple, Stupid)”原则。

2.3 C++语言特性的双刃剑

C++的强大在于其灵活性和控制力,但这也给良好设计带来了挑战。

  • 多重继承的陷阱 :C++支持多重继承,这本是模拟现实世界复杂关系的利器。但若滥用,极易导致恐怖的“菱形继承”问题,以及接口污染(一个类拥有太多不相关的接口)。
  • 友元的滥用 friend 关键字破坏了封装性。虽然在某些特定场景(如运算符重载)下很有用,但如果在业务类之间大量使用友元关系,就会让类之间的耦合变得紧密而隐蔽。
  • 宏定义的隐患 :用宏来实现“代码复用”(如泛型算法),是C时代的遗产。它缺乏类型安全,调试困难,且容易产生意想不到的副作用。在现代C++中,应优先使用模板、内联函数和 constexpr

2.4 缺乏对“变化方向”的预见

所有设计模式的核心目标都是管理变化。但很多开发者在设计时,没有认真思考:“未来哪些部分最有可能发生变化?”是数据来源?是算法策略?是输出格式?还是对象创建的方式?

如果封装了错误方向的“变化”,那么当真正的变化来临时,设计不仅没有帮助,反而成了枷锁。例如,你预判数据会从数据库来,所以用了一个抽象的数据读取接口。结果需求变成数据从消息队列来,你发现接口定义不适用,需要大改。这时,当初的抽象就做了无用功,甚至成了负担。

我的心得 :不要试图预测所有变化,那是不可能的。关键在于识别出那些在 当前业务上下文 中, 最直观、最可能 的变化点。例如,一个绘图程序,绘制“形状”的算法(如光栅化、矢量渲染)可能稳定,但“形状”的类型(圆形、矩形、三角形)肯定会增加。那么,我们的设计就应使“添加新形状”变得容易,而不是过度抽象渲染算法。

3. 设计模式实战:C++中的四把“手术刀”

设计模式是经验总结的“套路”,而非圣经。在C++中应用它们,需要结合语言特性进行“本土化”改造。下面我挑选四个最常用、也最能体现C++特色的模式,结合实例讲解如何运用。

3.1 策略模式(Strategy):解耦算法与对象

场景 :你的系统有一个核心算法,但这个算法有多种实现方式,并且需要在运行时动态切换。例如,一个数据压缩模块,需要支持ZIP、RAR、7Z等多种压缩算法;一个排序功能,需要支持快速排序、归并排序、堆排序。

传统困境 :你可能会在类内部用一堆 if-else switch-case 来判断使用哪种算法。这导致类职责过重,且增加新算法需要修改原有类,违反开闭原则。

C++实现要点

  1. 定义策略接口 :用一个抽象基类(或C++20的 concept )定义算法接口。
  2. 实现具体策略 :为每种算法实现一个具体的策略类。
  3. 组合使用 :在主体类中持有一个策略接口的指针或引用(通常用 std::unique_ptr 管理),并通过构造函数或设置方法注入具体的策略对象。
// 策略接口:压缩算法
class CompressionStrategy {
public:
    virtual ~CompressionStrategy() = default;
    virtual std::vector<uint8_t> compress(const std::vector<uint8_t>& data) = 0;
    virtual std::vector<uint8_t> decompress(const std::vector<uint8_t>& compressedData) = 0;
};

// 具体策略:ZIP压缩
class ZipCompression : public CompressionStrategy {
public:
    std::vector<uint8_t> compress(const std::vector<uint8_t>& data) override {
        // 调用ZIP库实现压缩
        std::cout << "Compressing using ZIP algorithm." << std::endl;
        return data; // 简化返回
    }
    // ... 实现 decompress
};

// 具体策略:RAR压缩
class RarCompression : public CompressionStrategy {
    // ... 类似实现
};

// 上下文类:压缩器
class DataCompressor {
private:
    std::unique_ptr<CompressionStrategy> strategy_;
public:
    // 通过构造函数注入策略
    explicit DataCompressor(std::unique_ptr<CompressionStrategy> strategy)
        : strategy_(std::move(strategy)) {}

    // 也可以在运行时动态设置策略
    void setStrategy(std::unique_ptr<CompressionStrategy> strategy) {
        strategy_ = std::move(strategy);
    }

    std::vector<uint8_t> compressData(const std::vector<uint8_t>& data) {
        if (!strategy_) {
            throw std::runtime_error("No compression strategy set!");
        }
        return strategy_->compress(data);
    }
};

// 使用示例
int main() {
    auto zipCompressor = std::make_unique<DataCompressor>(std::make_unique<ZipCompression>());
    auto data = std::vector<uint8_t>{1, 2, 3, 4, 5};
    auto compressed = zipCompressor->compressData(data);

    // 动态切换为RAR算法
    zipCompressor->setStrategy(std::make_unique<RarCompression>());
    compressed = zipCompressor->compressData(data);
    return 0;
}

C++特色与技巧

  • 使用智能指针管理策略对象 std::unique_ptr 明确了所有权,防止内存泄漏。
  • 考虑使用 std::function 替代接口 :如果策略只是一个简单的操作(比如一个比较函数),使用 std::function<bool(int, int)> 比定义接口类更轻量。这其实是策略模式的一种函数式实现。
  • 与模板结合 :如果策略类型在编译期就能确定,可以使用模板实现“编译期策略模式”,完全消除运行时多态的开销。
    template <typename CompressionStrategy>
    class DataCompressorTemplate {
        CompressionStrategy strategy_;
    public:
        std::vector<uint8_t> compressData(const std::vector<uint8_t>& data) {
            return strategy_.compress(data);
        }
    };
    // 使用:DataCompressorTemplate<ZipCompression> compressor;
    

3.2 工厂模式(Factory):封装对象创建的复杂性

场景 :对象创建逻辑复杂(例如需要读取配置、初始化依赖),或者你希望将创建逻辑与使用逻辑分离,以支持未来轻松添加新的产品类型。例如,创建不同类型的UI按钮(Windows风格、Mac风格、Linux风格),或者连接不同类型的数据库(MySQL、PostgreSQL、SQLite)。

C++实现要点(以工厂方法为例)

  1. 产品接口 :定义一个抽象基类,表示所有产品共有的接口。
  2. 具体产品 :实现具体的产品类。
  3. 工厂接口/基类 :声明一个创建产品的虚方法。
  4. 具体工厂 :重写工厂方法,返回具体的产品对象。
// 产品接口:数据库连接
class DatabaseConnection {
public:
    virtual ~DatabaseConnection() = default;
    virtual void connect(const std::string& connectionString) = 0;
    virtual void executeQuery(const std::string& query) = 0;
};

// 具体产品:MySQL连接
class MySqlConnection : public DatabaseConnection {
public:
    void connect(const std::string& connectionString) override {
        std::cout << "Connecting to MySQL: " << connectionString << std::endl;
        // 实际初始化 MySQL C API 或 ORM
    }
    void executeQuery(const std::string& query) override {
        std::cout << "Executing MySQL query: " << query << std::endl;
    }
};

// 具体产品:SqliteConnection 类似...

// 工厂基类
class ConnectionFactory {
public:
    virtual ~ConnectionFactory() = default;
    virtual std::unique_ptr<DatabaseConnection> createConnection() = 0;
};

// 具体工厂:MySQL连接工厂
class MySqlConnectionFactory : public ConnectionFactory {
public:
    std::unique_ptr<DatabaseConnection> createConnection() override {
        auto conn = std::make_unique<MySqlConnection>();
        // 这里可以进行一些MySQL连接特有的初始化配置
        // 例如,设置默认字符集、超时时间等
        return conn; // 隐式转换为 unique_ptr<DatabaseConnection>
    }
};

// 客户端代码
class Application {
    std::unique_ptr<ConnectionFactory> factory_;
public:
    explicit Application(std::unique_ptr<ConnectionFactory> factory)
        : factory_(std::move(factory)) {}

    void run() {
        auto connection = factory_->createConnection();
        connection->connect("host=localhost;user=root");
        connection->executeQuery("SELECT * FROM users");
    }
};

// 使用:根据配置决定使用哪个工厂
int main() {
    std::string dbType = "mysql"; // 可以从配置文件读取
    std::unique_ptr<ConnectionFactory> factory;

    if (dbType == "mysql") {
        factory = std::make_unique<MySqlConnectionFactory>();
    } else if (dbType == "sqlite") {
        // factory = std::make_unique<SqliteConnectionFactory>();
    } else {
        throw std::runtime_error("Unsupported database type");
    }

    Application app(std::move(factory));
    app.run();
    return 0;
}

C++特色与技巧

  • 返回 std::unique_ptr :工厂方法通常返回堆上对象的独占所有权, std::unique_ptr 是最佳选择。
  • 参数化工厂 :如果创建过程需要参数,可以传递给 createConnection 方法,或者保存在工厂对象内部状态中。
  • 简单工厂与静态方法 :对于不那么复杂的场景,可以只用一个“简单工厂”类,里面包含几个静态的创建方法,而不需要完整的工厂层次结构。这更简单,但扩展性稍差。
  • 利用RAII :确保工厂创建的产品在异常情况下也能被正确清理,智能指针自动管理生命周期是RAII思想的完美体现。

3.3 观察者模式(Observer):实现松耦合的事件通知

场景 :当一个对象(主题)的状态发生改变时,需要自动通知其他多个对象(观察者),而这些观察者之间彼此不知道对方的存在。典型应用:GUI中的事件处理、数据模型与视图的同步、日志系统、游戏中的成就系统。

C++实现要点

  1. 观察者接口 :定义观察者必须实现的更新方法。
  2. 具体观察者 :实现更新方法,定义在接到通知后要执行的具体操作。
  3. 主题(被观察者) :维护一个观察者列表,提供注册、注销观察者的方法,并在状态改变时遍历列表调用所有观察者的更新方法。

传统实现的问题 :主题需要知道观察者的具体类型(指针),存在生命周期管理问题(野指针)和紧耦合。

现代C++改进版 :使用 std::function 和弱引用解决。

#include <iostream>
#include <vector>
#include <functional>
#include <memory>
#include <algorithm>

// 主题:一个简单的数据模型
class DataModel {
private:
    int value_ = 0;
    // 使用 std::function 存储回调,避免继承。使用 weak_ptr 避免生命周期问题。
    std::vector<std::weak_ptr<std::function<void(int)>>> observers_;

    void notifyObservers() {
        // 移除已经失效的观察者
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                           [](const std::weak_ptr<std::function<void(int)>>& wp) {
                               return wp.expired();
                           }),
            observers_.end()
        );

        // 通知有效的观察者
        for (const auto& wp : observers_) {
            if (auto sp = wp.lock()) {
                (*sp)(value_);
            }
        }
    }

public:
    void setValue(int newValue) {
        if (value_ != newValue) {
            value_ = newValue;
            notifyObservers();
        }
    }

    int getValue() const { return value_; }

    // 注册观察者,返回一个 shared_ptr,观察者需要保存它以确保回调有效。
    std::shared_ptr<std::function<void(int)>> registerObserver(std::function<void(int)> callback) {
        auto callbackPtr = std::make_shared<std::function<void(int)>>(std::move(callback));
        observers_.emplace_back(callbackPtr);
        return callbackPtr;
    }
    // 注销通常不需要了,因为 weak_ptr 会自动失效。如果需要主动注销,可以设计一个基于 token 的机制。
};

// 具体观察者:一个UI显示组件
class DisplayUI {
private:
    std::shared_ptr<std::function<void(int)>> observerToken_;
public:
    void connectToModel(DataModel& model) {
        // 注册一个lambda作为回调
        observerToken_ = model.registerObserver([this](int value) {
            this->onDataChanged(value);
        });
    }

    void onDataChanged(int value) {
        std::cout << "[DisplayUI] Data changed to: " << value << std::endl();
    }
};

// 另一个观察者:日志记录器
class Logger {
private:
    std::shared_ptr<std::function<void(int)>> observerToken_;
public:
    void connectToModel(DataModel& model) {
        observerToken_ = model.registerObserver([this](int value) {
            std::cout << "[Logger] Model value updated: " << value << std::endl();
        });
    }
};

int main() {
    DataModel model;
    DisplayUI ui;
    Logger logger;

    ui.connectToModel(model);
    logger.connectToModel(model);

    model.setValue(10); // 两者都会收到通知
    model.setValue(20); // 两者都会收到通知

    // 当 ui 或 logger 对象析构时,其持有的 shared_ptr 被释放,
    // model 中的 weak_ptr 会过期,下次通知时会被自动清理。
    return 0;
}

C++特色与技巧

  • 使用 std::function :摆脱了观察者必须继承自某个接口的束缚,任何可调用对象(函数、lambda、bind表达式、函数对象)都可以成为观察者,灵活性极大提高。
  • 使用 std::weak_ptr 管理观察者生命周期 :这是关键。主题只持有观察者的弱引用( weak_ptr ),不控制其生命周期。观察者自身需要持有一个 shared_ptr 来保持回调有效性。当观察者对象销毁时, shared_ptr 计数归零,主题中的 weak_ptr 会失效,从而安全地避免悬空指针问题。
  • 线程安全考虑 :如果主题和观察者可能在不同线程被访问, observers_ 容器的修改和遍历需要加锁(如 std::mutex ),或者使用线程安全的容器。

3.4 单例模式(Singleton):谨慎使用的全局访问点

场景 :确保一个类只有一个实例,并提供一个全局访问点。常用于日志管理器、配置管理器、线程池、数据库连接池等。

C++传统实现(双检锁)及其问题

class Singleton {
private:
    static Singleton* instance_;
    static std::mutex mutex_;
    Singleton() {} // 私有构造函数
    ~Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    static Singleton* getInstance() {
        if (instance_ == nullptr) { // 第一次检查
            std::lock_guard<std::mutex> lock(mutex_);
            if (instance_ == nullptr) { // 第二次检查
                instance_ = new Singleton();
            }
        }
        return instance_;
    }
};
// 需要在cpp文件中初始化
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;

问题 :双检锁在C++11之前的内存模型下存在隐患(指令重排可能导致返回未完全构造的对象)。C++11之后,使用 std::atomic std::call_once 可以更安全地实现。

现代C++推荐方案:Meyers‘ Singleton (局部静态变量)

class Singleton {
private:
    Singleton() { std::cout << "Singleton constructed.\n"; }
    ~Singleton() { std::cout << "Singleton destroyed.\n"; }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    static Singleton& getInstance() {
        static Singleton instance; // C++11保证线程安全的局部静态初始化
        return instance;
    }
    void doSomething() { /* ... */ }
};

优点

  1. 线程安全 :C++11及以后标准保证,局部静态变量的初始化在多线程环境下是线程安全的。
  2. 懒加载 :只有在第一次调用 getInstance() 时才创建对象。
  3. 自动析构 :程序结束时,静态变量会自动析构,无需手动管理内存。
  4. 代码简洁 :无需手动管理指针和互斥锁。

重要注意事项

  • 单例的弊端 :单例本质上是一个全局变量,会带来隐藏的耦合,不利于单元测试(难以模拟),也可能导致资源竞争。 应谨慎使用,仅在确有必要时使用
  • 考虑依赖注入 :很多时候,将单例对象作为参数传递给需要它的函数或类(依赖注入),比在函数内部直接调用 Singleton::getInstance() 更好,这提高了可测试性和模块化。
  • 非可复制的单例 :务必删除拷贝构造函数和拷贝赋值运算符,确保实例唯一。

4. 超越经典模式:现代C++的复用利器

设计模式是思想,而现代C++(C++11/14/17/20)提供了更强大、更直接的语言工具来实现这些思想,甚至创造出新的、更简洁的复用范式。

4.1 模板与泛型编程:编译期多态

策略模式、工厂方法在运行时通过虚函数实现多态。而模板允许我们在编译期完成“多态”,效率更高,且能作用于非继承体系的类型。

场景 :编写一个通用的 calculate 函数,可以对任何支持 + * 操作的类型进行运算。

// 传统面向对象方法:需要定义接口和继承
class Calculable {
public:
    virtual ~Calculable() = default;
    virtual Calculable* add(const Calculable* other) const = 0;
    virtual Calculable* multiply(const Calculable* other) const = 0;
    // ... 还需要 clone 等,非常繁琐
};

// 模板方法:简洁、高效、类型安全
template <typename T>
T calculate(const T& a, const T& b) {
    return a * b + a; // 要求类型T支持 operator* 和 operator+
}

// 用于 int, double, 甚至自定义的复数类、矩阵类等
int main() {
    std::cout << calculate(3, 4) << std::endl; // 输出 15 (3*4+3)
    std::cout << calculate(2.5, 1.5) << std::endl; // 输出 6.25 (2.5*1.5+2.5)

    // 自定义类型
    struct Vec2 { int x, y; };
    // 需要为 Vec2 重载 operator* 和 operator+
    // auto result = calculate(Vec2{1,2}, Vec2{3,4}); // 如果重载了,就可以用
}

C++20概念(Concepts) :进一步约束模板参数,使错误信息更清晰。

template <typename T>
concept AddableMultipliable = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
    { a * b } -> std::same_as<T>;
};

template <AddableMultipliable T>
T calculate_v2(const T& a, const T& b) {
    return a * b + a;
}
// 如果使用不支持的类型,编译错误会明确指出不满足“AddableMultipliable”概念。

4.2 智能指针与RAII:自动化的资源管理复用

资源管理(内存、文件句柄、网络连接、锁)是C++编程中的重中之重。手动管理极易出错。RAII(Resource Acquisition Is Initialization)是C++的核心 idiom,而智能指针( std::unique_ptr , std::shared_ptr , std::weak_ptr )是其最典型的应用。

复用价值 :你不再需要在每个类里写重复的 new / delete ,或者担心异常安全。智能指针的逻辑被复用到了所有需要管理动态资源的场景。

// 不好的做法:手动管理
void processFile() {
    FILE* f = fopen("data.txt", "r");
    if (!f) return;
    // ... 操作文件
    if (someError) {
        fclose(f); // 每个错误出口都要记得关闭!
        return;
    }
    // ... 更多操作
    fclose(f); // 正常出口也要关闭
}

// 好的做法:复用RAII思想(使用现代C++的fstream或自定义RAII包装器)
class FileRAII {
    FILE* file_;
public:
    explicit FileRAII(const char* filename, const char* mode) : file_(fopen(filename, mode)) {
        if (!file_) throw std::runtime_error("Failed to open file");
    }
    ~FileRAII() { if (file_) fclose(file_); }
    FILE* get() const { return file_; }
    // 禁止拷贝
    FileRAII(const FileRAII&) = delete;
    FileRAII& operator=(const FileRAII&) = delete;
    // 允许移动
    FileRAII(FileRAII&& other) noexcept : file_(other.file_) { other.file_ = nullptr; }
    FileRAII& operator=(FileRAII&& other) noexcept { /*...*/ return *this; }
};

void processFileBetter() {
    FileRAII file("data.txt", "r"); // 资源在构造时获取
    // ... 操作 file.get()
    if (someError) {
        return; // 无需手动关闭,析构函数自动处理
    }
    // ... 更多操作
} // 离开作用域,file的析构函数自动调用 fclose

std::unique_ptr 与工厂模式 :如前所述,工厂模式返回 std::unique_ptr ,完美结合了创建逻辑封装和资源自动管理。

4.3 Lambda表达式与std::function:策略模式与回调的轻量化

在策略模式例子中,如果策略只是一个简单的行为(比如一个比较准则),使用 std::function 和lambda比定义一整个类层次要简洁得多。

// 使用 std::function 的策略模式
class Sorter {
public:
    using CompareFunc = std::function<bool(int, int)>;
    void sort(std::vector<int>& data, CompareFunc comp) {
        std::sort(data.begin(), data.end(), comp);
    }
};

int main() {
    Sorter sorter;
    std::vector<int> nums = {5, 2, 8, 1, 9};

    // 策略1:升序排序 (通过lambda注入)
    sorter.sort(nums, [](int a, int b) { return a < b; });
    // 策略2:降序排序
    sorter.sort(nums, [](int a, int b) { return a > b; });
    // 策略3:按绝对值排序
    sorter.sort(nums, [](int a, int b) { return std::abs(a) < std::abs(b); });

    return 0;
}

这种方式极大地减少了样板代码,特别适合一次性使用的简单策略。

4.4 移动语义与完美转发:高效资源转移的复用

移动语义(Move Semantics)和完美转发(Perfect Forwarding)是C++11引入的革命性特性,它们本身不是设计模式,但为实现高效、安全的资源管理(如工厂模式、构建器模式)提供了底层支持。

在工厂模式中的应用 :工厂创建的对象,可以通过移动语义高效地“转移”给调用者,避免不必要的拷贝。

std::unique_ptr<ExpensiveObject> ExpensiveObjectFactory::create() {
    auto obj = std::make_unique<ExpensiveObject>();
    obj->initializeWithHeavyResources(); // 进行昂贵的初始化
    return obj; // 这里发生移动构造,成本极低
}
// 调用方
auto obj = factory.create(); // 高效地获得对象所有权

在构建器模式(Builder Pattern)中的应用 :构建器模式用于分步构造复杂对象。结合移动语义,可以流畅地链式调用并最终高效地构建对象。

class QueryBuilder {
    std::string select_;
    std::string from_;
    std::vector<std::string> whereClauses_;
public:
    QueryBuilder& select(const std::string& columns) {
        select_ = "SELECT " + columns;
        return *this;
    }
    QueryBuilder& from(const std::string& table) {
        from_ = " FROM " + table;
        return *this;
    }
    QueryBuilder& where(const std::string& condition) {
        whereClauses_.push_back(" AND " + condition);
        return *this;
    }
    // build() 返回构建好的对象,内部使用移动语义
    std::string build() && { // 右值引用限定符,表示只能在临时对象上调用
        std::string query = std::move(select_) + std::move(from_);
        if (!whereClauses_.empty()) {
            query += " WHERE 1=1";
            for (auto& clause : whereClauses_) {
                query += std::move(clause);
            }
        }
        return query; // 返回移动构造的字符串
    }
};

// 使用
auto query = QueryBuilder().select("*").from("users").where("age > 18").where("status=1").build();
// QueryBuilder() 是临时对象,可以调用右值限定的 build(),内部字符串被高效移动。

5. 实战心法:如何选择与组合模式?

知道了这么多模式和工具,面对具体问题该如何选择?我的经验是遵循一个简单的决策流程:

  1. 识别变化点 :首先问自己,这个模块或功能中,哪些部分在未来最可能变化?是算法?是对象类型?是事件响应?还是对象创建方式?
  2. 评估复杂度 :这个变化点带来的复杂度有多高?如果只是简单的行为变化(如排序规则),一个 std::function 或lambda可能就够了(轻量策略)。如果需要管理一系列相关对象的创建,可能需要抽象工厂。
  3. 考虑生命周期与依赖 :对象之间的关系是暂时的还是永久的?是否需要全局访问?观察者模式需要小心管理生命周期,单例模式要警惕全局状态。
  4. 优先使用组合而非继承 :这是GoF设计模式的核心原则之一。通过包含(has-a)而非继承(is-a)来复用功能,能使系统更灵活。模板编程和基于 std::function 的策略都是组合的体现。
  5. KISS和YAGNI原则 Keep It Simple, Stupid (保持简单)和 You Ain‘t Gonna Need It (你不会需要它)。不要为了用模式而用模式。如果当前需求用简单的 if-else 或一两个函数就能清晰、稳定地解决,那就不要引入复杂的模式。当变化真正到来,且现有代码难以优雅扩展时,再考虑重构引入模式。
  6. 利用现代C++特性 :在实现经典模式时,时刻思考能否用 std::unique_ptr std::function 、lambda、移动语义、模板等现代特性来简化实现、提高性能或增强安全性。

组合模式示例 :一个游戏角色系统。角色(Character)有一个武器(Weapon)和一个移动策略(MovementStrategy)。

  • Weapon 可以使用策略模式,让角色能在运行时切换剑、弓、法杖。
  • MovementStrategy 也可以使用策略模式,处理行走、奔跑、飞行等。
  • Character 通过组合的方式拥有 Weapon MovementStrategy 的指针或 std::unique_ptr ,而不是通过继承多个武器类或移动类。这样,增加新武器或新移动方式,完全不需要修改 Character 类,只需创建新的策略类并注入即可。

6. 常见陷阱与调试技巧

即使理解了原理,在实际编码中依然会踩坑。这里记录几个我印象深刻的教训和排查方法。

6.1 虚析构函数遗忘

这是使用继承和多态时最常见的错误之一。

class Base {
public:
    // ~Base() {} // 错误!如果不是虚析构函数
    virtual ~Base() = default; // 正确
    virtual void doSomething() = 0;
};
class Derived : public Base { /* ... */ };

Base* ptr = new Derived();
delete ptr; // 如果Base的析构函数非虚,这里会导致未定义行为(通常只调用~Base(),不调用~Derived())

排查 :对于任何打算作为基类(尤其是含有虚函数)的类,第一反应就是为其声明虚析构函数。现代C++中,直接写 virtual ~ClassName() = default; 是最佳实践。

6.2 智能指针的循环引用

使用 std::shared_ptr 时,如果两个对象互相持有对方的 shared_ptr ,会导致引用计数永远不为0,内存泄漏。

class Node {
public:
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev; // 如果双向链表都用 shared_ptr,就会形成循环引用
};

解决方案 :将其中一个指针改为 std::weak_ptr weak_ptr 不增加引用计数,只观察对象,需要使用时通过 lock() 方法尝试获取一个临时的 shared_ptr

class Node {
public:
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev; // 使用 weak_ptr 打破循环
};

6.3 多线程环境下的数据竞争

许多设计模式(如单例、观察者、工厂)在单线程下工作良好,但在多线程下可能出问题。

  • 懒汉式单例(非Meyers版) :需要双检锁或 std::call_once 保证线程安全。
  • 观察者模式 :在 notifyObservers() 中遍历观察者列表时,如果另一个线程正在注册或注销观察者,会导致迭代器失效或访问冲突。
  • 工厂模式 :如果工厂内部有缓存(如对象池),缓存的访问需要同步。

排查与解决

  1. 明确线程模型 :先搞清楚哪些对象是线程局部的,哪些是共享的。
  2. 使用互斥锁( std::mutex :保护共享数据的访问。注意锁的粒度,避免死锁。
  3. 考虑无锁编程或原子操作 :对于简单的计数器、标志位,使用 std::atomic
  4. 使用线程安全容器 :C++17提供了 std::shared_mutex (读写锁),对于读多写少的观察者列表很有用。

6.4 过度使用设计模式导致代码晦涩

我曾接手过一个项目,几乎每个类都是某个模式的产物,为了创建一个简单的对话框,需要跟踪十多个类的交互。这严重降低了开发效率和代码可读性。

如何识别和重构

  • 症状 :简单任务需要穿越很多层抽象;添加一个小功能需要修改多个文件;新人需要一周才能看懂一个模块的基本流程。
  • 重构方向
    • 合并过度拆分的类 :如果两个类总是一起变化,且其中一个除了为另一个提供接口外别无他用,考虑合并。
    • 用简单条件判断替代工厂 :如果产品类型很少(比如就两三种),且不太可能增加,用简单的 if-else switch 可能比完整的工厂模式更清晰。
    • 用回调替代观察者 :如果观察者只有一个,或者通知逻辑非常简单,直接传递一个 std::function 回调函数可能更直接。

6.5 性能分析工具的使用

引入设计模式,特别是基于虚函数的多态,会带来一定的运行时开销(虚表查找、间接调用)。对于性能关键路径,需要评估。

  • 使用性能剖析器(Profiler) :如 perf (Linux)、 VTune (Intel)、 Visual Studio Profiler 等。找到真正的热点(hot path)。
  • 对比测试 :对于关键算法,可以编写基准测试,对比使用策略模式(运行时多态)和模板(编译期多态)的性能差异。很多时候,差异可以忽略不计;但在每秒处理数百万次的循环中,差异可能被放大。
  • 记住“不要过早优化” :在绝大多数业务逻辑中,设计模式带来的清晰度和可维护性收益,远大于其微小的性能损耗。先写出清晰、正确的代码,再用剖析器指导优化。

解决C++代码复用与设计模式的难题,没有银弹。它是一场在 灵活性 清晰度 性能 开发效率 之间持续的权衡。我的经验是,从理解“封装变化”这一根本原则出发,熟练掌握现代C++提供的各种工具(智能指针、lambda、模板等),并在实际项目中反复练习和反思。开始时可能会设计过度或不足,这很正常。重要的是培养一种直觉:当代码散发出“坏味道”(如重复、冗长、难以修改)时,能够识别出是哪一种设计模式或编程技巧可以优雅地解决它。最终目标,是让代码自己说话,清晰表达其意图,并能从容应对未来的变化。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐