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.
在现代 C++ 中, 有一些通用的设计模式, 编程思想和惯用法, 可以帮助我们编写更高效, 可维护和安全的代码. 下面介绍几个常见的设计模式和编程惯用法.
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 (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总之链式调用带来的流畅性和可读性提升是显而易见的, 用来实现函数式编程或者声明式风格的接口也是非常合适的.