HomeArchiveBlog


Original contents are licensed under CC BY-NC 4.0. All rights reserved © 2026 Kai.
Back to Blog
Design Patterns in Modern C++

This document introduces several C++ design patterns and idioms. RAII manages resource lifetimes via object scope. CRTP enables static polymorphism, reducing runtime overhead. The Singleton pattern ensures a single instance with global access. Chaining calls enhance code readability and expressiveness for sequential operations.

Wed Oct 29 2025
Mon Dec 15 2025
C++Design PatternsRAII
On this page
  • 设计模式
    • RAII
    • CRTP
    • 单例模式
    • 链式调用

设计模式

在现代 C++ 中, 有一些通用的设计模式, 编程思想和惯用法, 可以帮助我们编写更高效, 可维护和安全的代码. 下面介绍几个常见的设计模式和编程惯用法.

RAII

RAII (Resource Acquisition Is Initialization, 资源获取即初始化) 是 C++ 中一种重要的编程惯用法, 用来管理资源的生命周期. RAII 的核心思想是将资源的获取和释放绑定到对象的生命周期上, 通过对象的构造函数获取资源, 在析构函数中释放资源. 这样可以确保资源在不再需要时被正确释放, 避免内存泄漏和资源泄漏等问题. 典型的 RAII 示例就是智能指针, 它们在构造时获取内存资源, 在析构时释放内存资源. 例如:

#include <memory>
void foo() {
    std::unique_ptr<int[]> p(new int[100]); // acquire resource
    // use the resource
} // p goes out of scope here, resource is automatically released

在上面的例子中, std::unique_ptr 对象 p 在函数 foo 中被创建, 它在构造时获取了一块动态内存资源. 当函数执行完毕, p 超出作用域时, 它的析构函数会自动被调用, 释放这块内存资源. 这样就确保了内存资源不会泄漏.

RAII 的优势在于它简化了资源管理的复杂性, 避免了手动释放资源时可能出现的错误. 通过将资源的获取和释放绑定到对象的生命周期上, 可以确保资源在任何情况下都能被正确释放, 包括异常发生时. 例如:

#include <memory>
void foo() {
    std::unique_ptr<int[]> p(new int[100]); // acquire resource
    // do something that may throw an exception
    if (some_error_condition) {
        throw std::runtime_error("error occurred");
    }
    // use the resource
} // p goes out of scope here, resource is automatically released even if an exception is thrown

在上面的例子中, 即使在使用资源的过程中发生了异常, p 的析构函数仍然会被调用, 释放内存资源. 这样就避免了内存泄漏的问题.

RAII 不仅适用于内存资源的管理, 还可以用于文件句柄, 网络连接, 互斥锁等各种资源的管理. 只要将资源的获取和释放绑定到对象的生命周期上, 就可以利用 RAII 来简化资源管理的复杂性.

CRTP

CRTP (Curiously Recurring Template Pattern, 奇异递归模板模式) 是一种利用模板实现静态多态的设计模式. 它通过让派生类继承自一个以自身类型为模板参数的基类, 实现编译期的多态行为.

动态多态通常通过虚函数实现, 虚函数本身是通过维护一个虚函数表(vtable)来实现的, 会导致一定的运行时开销, 而且关键的一点是, 很多时候我们也不需要动态多态的灵活性, 只需要在编译期就确定好类型和行为即可. 这时候 CRTP 就派上用场了.

template<typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->impl();
    }
}

class Derived1 : public Base<Derived1> {
public:
    void impl() {
        std::cout << "Derived1 implementation" << std::endl;
    }
};

class Derived2 : public Base<Derived2> {
public:
    void impl() {
        std::cout << "Derived2 implementation" << std::endl;
    }
};

然后我们在使用的时候, 就可以使用通用的 interface 接口来调用派生类的具体实现:

Derived1 d1;
d1.interface(); // prints "Derived1 implementation"
Derived2 d2;
d2.interface(); // prints "Derived2 implementation"

CRTP 的模式是, 先在基类中定义一个通用接口, 在这个接口中通过 static_cast 将 this 指针转换为派生类类型, 然后调用派生类的具体实现方法. 在上面这种编写方式下, 会强制所有派生类都必须实现 impl 方法, 否则编译器在特化模板时就会因为找不到 Derived::impl() 的定义而报错.

CRTP 同样可以像动态多态一样提供一个默认实现, 例如

template<typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->impl();
    }
    // provide a default implementation
    void impl() {
        std::cout << "Base implementation" << std::endl;
    }
}

class Derived : Base<Derived> {
public:
    // void impl();
}

这个时候调用 Derived::interface() 方法就是使用基类中的默认实现方式.

使用 CRTP 的好处在于可以实现静态多态, 提升运行时性能, 但缺点是可能会导致二进制膨胀. 后者是模板编程中很容易遇到的问题, 大部分情况下一点二进制膨胀也无关紧要.

单例模式

单例模式(Singleton Pattern)是一种常见的设计模式, 用来确保一个类只有一个实例, 并提供一个全局访问点, 比如用在提供一个全局上下文对象的场景中. C++11 中引入了 std::call_once 和 std::once_flag 用来实现线程安全的单例模式.

#include <mutex>
class Singleton {
public:
    static Singleton& getInstance() {
        std::call_once(initFlag, []() {
            instance.reset(new Singleton());
        });
        return *instance;
    }
private:
    Singleton() {} // private constructor
    Singleton(const Singleton&) = delete; // disable copy constructor
    Singleton& operator=(const Singleton&) = delete; // disable assignment operator
    static std::unique_ptr<Singleton> instance;
    static std::once_flag initFlag;
};
std::unique_ptr<Singleton> Singleton::instance;
std::once_flag Singleton::initFlag;

这里有几个操作用来保证单例的唯一性和线程安全:

  • 私有构造函数, 这样外部无法直接创建实例.
  • 删除拷贝构造函数和赋值运算符, 防止通过拷贝或者赋值创建新的实例.
  • std::call_once 用来确保即使在多线程环境下, 也只会初始化一次实例. 它要配合 std::once_flag 一起使用.
  • 使用 std::unique_ptr 来管理单例实例的生命周期, 保证程序结束时自动释放资源.

需要注意的是, instance 和 initFlag 还要在类外进行定义和初始化, 在类中定义这两个成员仅仅是告诉编译器它们的存在. 这种实现方式确保了单例实例在第一次调用 getInstance 时被创建, 并且在多线程环境下也是安全的.

链式调用

链式调用(Chaining Calls)是一种编程风格, 通过返回对象本身或者其他相关对象, 允许连续调用多个方法, 常见于构建器模式(Builder Pattern)或者各种需要一系列连续操作的场景. 我们先看这个示例, 传统方法下, 如果我想要配置一个汽车对象的属性, 然后根据这些属性来创建汽车, 可能会写成这样:

struct Config {
    std::string color;
    int horsepower;
    bool sunroof;
};

class Car {
public:
    Car(const Config& config) 
        : color(config.color), horsepower(config.horsepower), sunroof(config.sunroof) {}
    void show() {
        std::cout << "Car color: " << color 
                  << ", Horsepower: " << horsepower 
                  << ", Sunroof: " << (sunroof ? "Yes" : "No") << std::endl;
    }
private:
    std::string color;
    int horsepower;
    bool sunroof;
};

// create a configuration object
Config config {"Red", 300, true};
// create a car object using the configuration
Car myCar(config);
myCar.show();

在这个例子中, 我们首先创建了一个 Config 对象来存储汽车的配置参数, 然后将其传递给 Car 类的构造函数来创建汽车对象. 另一种方式是不创建结构体, 直接将不同的参数传递给构造函数, 但如果参数过多, 代码会变得难以阅读和维护( 所以参数量大的话一般不会这么干).

而链式调用可以让这个过程显得更加简洁和直观.

class Car {
public:
    Car& setColor(const std::string& c) {
        color = c;
        return *this;
    }
    Car& setHorsepower(int hp) {
        horsepower = hp;
        return *this;
    }
    Car& setSunroof(bool sr) {
        sunroof = sr;
        return *this;
    }
    void show() {
        std::cout << "Car color: " << color 
                  << ", Horsepower: " << horsepower 
                  << ", Sunroof: " << (sunroof ? "Yes" : "No") << std::endl;
    }
    Car& build() {
        // could add validation or finalization logic here
        return *this;
    }
private:
    std::string color;
    int horsepower;
    bool sunroof;
};

Car c = Car()
        .setColor("Red")
        .setHorsepower(300)
        .setSunroof(true)
        .build();

在这个例子中, Car 类提供了一系列的设置方法, 每个方法都返回对象本身的引用(*this, 这是关键!), 这样就可以连续调用多个方法来配置汽车对象的属性. 最后调用 build 方法来完成对象的创建(这里可以添加一些验证或者最终化逻辑). 这种方法显然在语义上要强得多.

链式调用另外一个决定性的优势是, 它带来的更好的流畅性允许我们更自然地表达一系列操作, 例如使用 Lambda 表达式或者一些 std::function 函数对象来传递一组操作:

DataStream stream;
stream.filter([](const Data& d) { return d.value > 10; })
      .transform([](const Data& d) { return d.value * 2; })
      .sort([](const Data& a, const Data& b) { return a.value < b.value; })
      .for_each([](const Data& d) { std::cout << d.value << std::endl; });

这看起来有点像 std::views::filter 那种风格.

链式调用还可以用来实现延迟执行和惰性求值, 例如我们可以传入一些函数对象, 但并不立即执行, 而是在最后调用 execute 方法时才统一执行:

class TaskBuilder {
public:
    TaskBuilder& addTask(std::function<void()> task) {
        tasks.push_back(task);
        return *this;
    }
    void execute() {
        for (const auto& task : tasks) {
            task(); // execute each task
        }
    }
private:
    std::vector<std::function<void()>> tasks;
};

// calling will be like this:
TaskBuilder builder;
builder.addTask([]() { /* task 1 */ })
       .addTask([]() { /* task 2 */ })
       .addTask([]() { /* task 3 */ })
       // tasks are not executed yet
       .execute(); // execute all tasks

总之链式调用带来的流畅性和可读性提升是显而易见的, 用来实现函数式编程或者声明式风格的接口也是非常合适的.