聊聊 C++ RAII

1. RAII

代码中常常需要管理资源,可能是内存,可能是锁,或者是某个 handle 等等,下面抽象了 acquire, release 两个接口分别表示获取和释放资源。

1.1. 问题场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void *acquire(int size) {
std::cout << __FUNCTION__ << " " << size << std::endl;
return malloc(size);
}

void release(void *ptr) {
free(ptr);
std::cout << __FUNCTION__ << std::endl;
}

void doSomething() {
void *ptr = acquire(100);
// use of ptr
// ...

release(ptr);
}

int main() {
doSomething();
}

会有如下打印

1
2
acquire 100
release

上面是 C 风格实现,这种实现需要程序员保证获取和释放一一对应,所以常常导致资源没释放,或者重复释放的问题。

RAII 意思是在 scope 内申请的资源在退出 scope 的时候自动释放,不用程序员来管理。从而极大地解放了程序员的负担,也从根本上杜绝资源的泄漏和重复释放。

2. 使用 smart ptr

C++ built-in 的 smart ptr 具备 RAII 能力。

2.1. 指定 deleter

这样可以用 unique_ptr 的 RAII 机制,当 doSomething() 结束时,会调用 deleter 函数,也就是 release。

实际上 acquire,release 是对 malloc,free 的封装,如果就用 malloc,free 的话是不需要特别指定的。

1
2
3
4
void doSomething() {
auto ptr =
std::unique_ptr<void, decltype(&release)>(acquire(100), release);
}

但是这种做法有一种情况似乎不好处理,当 acquire 定义如下,就比较麻烦了。

1
void acquire2(void **ptr, int size)

对于上面处理不了的情形,可以用下面的办法,可以用。

1
2
3
4
5
6
void doSomething() {
void *ptr = nullptr;
acquire2(&ptr, 100);
auto gen_ptr =
std::unique_ptr<void, decltype(&release)>(std::move(ptr), release);
}

这种实现不推荐,有以下两个原因

  1. 长得丑;
  2. ptr 还在,很可能被拿来做别的事情,也可能被 double free;

2.2. 使用 make_unique()

对需要管理的资源封装成一个资源管理类,在 constructor, destructor 中进行申请和释放。

make_unique是C++14引入的函数,它提供了一个更简单安全的方式来创建std::unique_ptr。

std::make_unique可以避免手动使用new来创建对象,它将对象创建和内存分配合并到一步,这样可以避免手动delete带来的潜在危险。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CustomMemoryMan {
public:
CustomMemoryMan(int size) { ptr_ = acquire(size); }
~CustomMemoryMan() { release(ptr_); }

private:
void *ptr_ = nullptr;
};


void doSomething() {
auto mem = std::make_unique<CustomMemoryMan>(100);

// use of gen_ptr
// ...
}

3. Summary

推荐最后的版本,即封装一个资源管理类,再使用 make_unique 来构建。