C++ 里的 RAII 是什么

2024-02-23 pv

写 C++ 的人经常会遇到一个词,RAII(Resource Acquisition Is Initialization)。初看令人疑惑。

展开看,中文翻译让人更困惑了:资源获取即初始化

什么是资源?什么叫初始化?这两者又存在什么关系?

今天这篇文章,就是想从三个方面简单谈谈,什么是 RAII,怎么使用,以及对于我的启发。

1. 一个通俗的例子

空谈 RAII 也许不容易理解。

有个图书馆。图书馆里有很多书。图书馆要借书给人看。

如果借的书没有及时归还,图书馆里的书就会越来越少。

因此图书馆有个规定:借书的时候要登记,给定时间内要办理归还手续

于是图书馆便借助一借一还两个流程,实现了书籍的管理。

2. 举个代码的例子

从一个简单的例子开始。

// Generate from GPT-3.5
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 创建一个互斥锁
void worker() {
std::lock_guard<std::mutex> lock(mtx); // 在作用域内创建 lock_guard 对象,自动获取互斥锁
std::cout << "Thread " << std::this_thread::get_id() << " is working." << std::endl;
// 在这里可以安全地访问共享资源,因为锁已经被 lock_guard 获取了
// 作用域结束时,lock_guard 对象析构,自动释放互斥锁
}
int main() {
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
return 0;
}

这里把std::mutex当作是一份资源。资源的使用通过lock/unlock实现。

但这里并没有显式使用这两个函数。相反,借助std::lock_guard一个辅助类间接管理std::mutex

给不熟悉 C++ 的读者简单解释下。主要分为两点。

  1. C++ 提供给开发者多个关于对象生命周期的,用前端的行话说,Hook 函数。其中最主要的就是构造函数析构函数,分别在对象构造和析构的时候执行
  2. C++ 中栈上对象的生命周期在一对{}内,因此构造函数会在进入括号内执行,析构函数执行于离开括号时

如此一来,std::lock_guard的极简实现就很清楚了:在类构造的时候执行lock,在析构的时候unlock即可。

好处也很明显。

一方面不需要显式地加锁/释放锁(自动),另一方面因为构造/析构函数一定会结伴执行,因此不会存在加锁后忘记释放的情况,降低开发者的心智负担(安全)。

这里使用到的技巧,就叫做:RAII。

通俗来说,按照个人理解,就是将资源的管理放在类的构造和析构函数中,如此,类的生命周期就是资源的使用周期

RAII 使得资源管理自动化,自动化则意味着更低的犯错可能。对于开发者而言,只要考虑清楚构造和析构函数中应该做的事,就可以解放双手,拥抱美好生活了。

这里的资源,不仅限于内存、网络连接、文件句柄等,任何你需要精确掌控的对象,都可以借助 RAII 管理。

比如,在 C 语言中经常出现的内存泄漏问题,在 C++11 中,通过智能指针(smart pointer)一劳永逸地解决了。其核心思想也是 RAII:在类构造的时候申请堆上内存,在析构的时候释放。

忘记 free、double free、野指针等问题便被很好的规避了。

由此可见,RAII 是个利器。

3. 一些启示

人是很容易犯错的。

关于这点,墨菲定律很早就诠释了。有人把它理解为某种“诅咒”,不过我倒觉得这未尝不是一种启示:既然墨菲定律不可避免,我们就应该尽早想办法规避

就像内存管理。既然手动维护容易出错,那我们就让这个管理自发进行。

自动化是个很好的方法。因为它的流程是确定的,过程是可复现的,人的因素被大大降低。因此在平时,如果有办法,我们便应该将任务委托,委托给计算机,委托给机器,委托给靠谱的人。当然,自己也要努力变得靠谱,被别人很好地使用。

另一点,便是有始有终。或者用高级点的词描述:闭环。

用我之前在百度时经常听到的一句话概括,便是:

凡事有交代,件件有着落,事事有回音。

(完)

参考

在 GitHub 上编辑本页面

最后更新于: 2024-06-14T07:12:04+08:00