C++ RAII 与 ScopeGuard

RAII机制

RAII(Resource Acquisition Is Initialization),也就是“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。

当我们在一个函数内部使用局部变量,当退出了这个局部变量的作用域时,这个变量也就别销毁了;当这个变量是类对象时,这个时候,就会自动调用这个类的析构函数,而这一切都是自动发生的,不要程序员显示的去调用完成。这个也太好了,RAII就是这样去完成的。

由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。

RAII 过程可以分为四个步骤:

  • 设计一个类封装资源;
  • 在构造函数中初始化;
  • 在析构函数中执行销毁操作;
  • 使用时声明一个该对象的类;

先看一个不使用RAII的例子,在下面的UseFile函数中:

void UseFile(char const* fn)
{
FILE* f = fopen(fn, "r"); // 获取资源
// 使用资源
if (!g()) { fclose(f); return; }
// ...
if (!h()) { fclose(f); return; }
// ...
fclose(f); // 释放资源
}

在使用文件f的过程中,因某些操作失败而造成函数提前返回的现象经常出现。这时函数UseFile的执行流程将变为:

现在的问题是:用于释放资源的代码fclose(f)需要在不同的位置重复书写多次。如果再加入异常处理,情况会变得更加复杂。例如,在文件f的使用过程中,程序可能会抛出异常:

void UseFile(char const* fn)
{
FILE* f = fopen(fn, "r"); // 获取资源
// 使用资源
try {
if (!g()) { fclose(f); return; }
// ...
if (!h()) { fclose(f); return; }
// ...
}
catch (...) {
fclose(f); // 释放资源
throw;
}
fclose(f); // 释放资源
}

我们必须依靠catch(...)来捕获所有的异常,关闭文件f,并重新抛出该异常。随着控制流程复杂度的增加,需要添加资源释放代码的位置会越来越多。如果资源的数量还不止一个,那么程序员就更加难于招架了。

如果使用RAII 的思想,

class FileHandle {
public:
FileHandle(char const* n, char const* a) { p = fopen(n, a); }
~FileHandle() { fclose(p); }
private:
// 禁止拷贝操作
FileHandle(FileHandle const&);
FileHandle& operator= (FileHandle const&);
FILE *p;
};

FileHandle类的构造函数调用fopen()获取资源;FileHandle类的析构函数调用fclose()释放资源。请注意,考虑到FileHandle对象代表一种资源,它并不具有拷贝语义,因此我们将拷贝构造函数和赋值运算符声明为私有成员。如果利用FileHandle类的局部对象表示文件句柄资源,那么前面的UseFile函数便可简化为:

void UseFile(char const* fn)
{
FileHandle file(fn, "r");
// 在此处使用文件句柄f...
// 超出此作用域时,系统会自动调用file的析构函数,从而释放资源
}

UserFile函数返回时,局部变量file生存期已过,会自动调用其析构函数。

如若使用文件file的代码中有异常抛出,难道析构函数还会被调用吗?此时RAII还能如此奏效吗?问得好。事实上,当一个异常抛出之后,系统沿着函数调用栈,向上寻找catch子句的过程,称为栈辗转开解(stack unwinding)。C++标准规定,在辗转开解函数调用栈的过程中,系统必须确保调用所有已创建起来的局部对象的析构函数。例如:

void Foo()
{
FileHandle file1("n1.txt", "r");
FileHandle file2("n2.txt", "w");
Bar(); // 可能抛出异常
FileHandle file3("n3.txt", "rw")
}

当调用Bar()时,局部对象file1和file2已经在Foo的函数调用栈中创建完毕,而file3却尚未创建。如果Bar()抛出异常,那么file2和file1的析构函数会被先后调用(注意:析构函数的调用顺序与构造函数相反);由于此时栈中尚不存在file3对象,因此它的析构函数不会被调用。只有当一个对象的构造函数执行完毕之后,我们才认为该对象的创建工作已经完成。栈辗转开解过程仅调用那些业已创建的对象的析构函数。

再看一个多线程同步的例子:

int sum = ;
std::mutex m; void add()
{
m.lock();
for (int n=; n<; n++) {
sum++ ;
}
m.unlock();
} int main()
{
std::thread t1(add);
std::thread t2(add);
t1.join();
t2.join(); std::cout << sum << std::endl;
}

对锁的管理(lock、unlock)也可以用RAII风格的方式:

int sum = ;
std::mutex lck; class MyMutex {
public:
MyMutex(std::mutex* p): plck(p) {
plck->lock();
}
~MyMutex() {
plck->unlock();
} private:
std::mutex* plck;
}; void add()
{
MyMutex mu(&lck);
for (int n=; n<; n++) {
sum++ ;
}
} int main()
{
std::thread t1(add);
std::thread t2(add);
t1.join();
t2.join(); std::cout << sum << std::endl;
}

事实上,std::lock_guard<T> 就是类似的实现。

综上所述,RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源必被释放。换句话说,拥有对象就等于拥有资源,对象存在则资源必定存在。由此可见,RAII惯用法是进行资源管理的有力武器。

ScopeGuard

ScopeGuard 最大的用处也是释放资源。

比如分配内存,做某些操作,再释放内存,很多人会这样写:

void* data = malloc(size);
xxxxx
if (xxx) {
xxxxx
}
else {
xxxxx
} free(data);

这样的代码是很脆弱的,有下面问题。

  • malloc 和 free 可能会相隔很远,难以看出它们的对应关系。
  • 另外中间有任何异常,中途 return,free 都不能被执行,就有资源泄露。
  • 假如将这段代码复用,搬到另外地方,容易漏掉 free。

为解决资源释放问题,有些老旧的 C/C++ 代码,会采用折衷写法,会使用 do {} while(false),甚至会用到 goto。 比如:

do {
if (xxx) {
break;
} if (xxx) {
break;
} xxx 正常操作
return ;
} while(false); free(data);
xxxx 释放资源
return ;

或者

 if (xxxx) {
goto fail;
} if (xxxx) {
goto fail;
} xxx 正常操作
return ; fail:
free(data);
xxxx 释放资源
return ;

对比 ScopeGuard 的写法:

void* data = malloc(size);
ON_SCOPE_EXIT {
free(data);
}; xxxxx
if (xxx) {
xxxxx
}
else {
xxxxx
}

资源一旦分配,接下来就立即使用 ScopeGuard 释放资源,这样分配和释放就会靠在一起,当退出作用域的的时候,里面的语句就被执行,释放掉资源。

无论下面的语句抛异常也好,中途退出也好,代码都是安全的。这样的代码也更容易修改,比如将其移动到另外的地方,它还是安全的。假如分配和释放分隔两地,移动代码时就很容易漏掉某些语句。

在Golang语言中,有defer关键字可以实现上面例子中的scope guard,而C++需要自己实现。

C++ 实现用到 C++ 11 的 lamda,定义对象将 lamda 存起来,在析构函数中调用。这个 lamda 在不同的场合也有不同的叫法,比如匿名函数,闭包,代码块,block。

有些小地方需要注意:一个是存储 lamda 使用了模板,而不是 std::function, 这个可以避免 lamda 转 std::function 的开销(尽管这个开销在绝大多数情况下可以忽略不计)。

#ifndef __SCOPE_GUARD_H__
#define __SCOPE_GUARD_H__ #define __SCOPEGUARD_CONCATENATE_IMPL(s1, s2) s1##s2
#define __SCOPEGUARD_CONCATENATE(s1, s2) __SCOPEGUARD_CONCATENATE_IMPL(s1, s2) #if defined(__cplusplus) #include <type_traits> // ScopeGuard for C++11
namespace clover {
template <typename Fun>
class ScopeGuard {
public:
ScopeGuard(Fun &&f) : _fun(std::forward<Fun>(f)), _active(true) {
} ~ScopeGuard() {
if (_active) {
_fun();
}
} void dismiss() {
_active = false;
} ScopeGuard() = delete;
ScopeGuard(const ScopeGuard &) = delete;
ScopeGuard &operator=(const ScopeGuard &) = delete; ScopeGuard(ScopeGuard &&rhs) : _fun(std::move(rhs._fun)), _active(rhs._active) {
rhs.dismiss();
} private:
Fun _fun;
bool _active;
}; namespace detail {
enum class ScopeGuardOnExit {}; template <typename Fun>
inline ScopeGuard<Fun> operator+(ScopeGuardOnExit, Fun &&fn) {
return ScopeGuard<Fun>(std::forward<Fun>(fn));
}
} // namespace detail
} // namespace clover // Helper macro
#define ON_SCOPE_EXIT \
auto __SCOPEGUARD_CONCATENATE(ext_exitBlock_, __LINE__) = clover::detail::ScopeGuardOnExit() + [&]() #else // ScopeGuard for Objective-C
typedef void (^ext_cleanupBlock_t)(void);
static inline void ext_executeCleanupBlock(__strong ext_cleanupBlock_t *block) {
(*block)();
} #define ON_SCOPE_EXIT \
__strong ext_cleanupBlock_t __SCOPEGUARD_CONCATENATE(ext_exitBlock_, __LINE__) \
__attribute__((cleanup(ext_executeCleanupBlock), unused)) = ^ #endif #endif /* __SCOPE_GUARD_H__ */

C++ RAII 与 ScopeGuard的更多相关文章

  1. C++11的资源管理:泛化的RAII

    RAII被认为是c++资源管理的最佳范式,但是c++98中用RAII必须为要管理的资源写一个类,这样一来RAII的使用就有些繁琐了.C++11有了lambda和function后,我们就可以编写泛化的 ...

  2. C++11的新特性lambda的小试牛刀RAII

    C/C++的资源是手动管理的 这导致程序员在申请资源时,最后用完了偶尔会忘记回收 C++语言的发明者倡导RAII,资源获取即初始化 使用对象来管理资源的生命周期,在超出作用域时,析构函数自动释放资源 ...

  3. c++异常处理第四篇---不使用try catch语句,使用Loki::ScopeGuard

    转载:神奇的Loki::ScopeGuard 2011-07-05 12:52:05 分类: C/C++ 转载:http://blog.csdn.net/fangqu/article/details/ ...

  4. 第25课 可变参数模板(6)_function_traits和ScopeGuard的实现

    1. function_traits (1)function_traits的作用:获取函数的实际类型.返回值类型.参数个数和具体类型等.它能获取所有函数语义类型信息.可以获取普通函数.函数指针.std ...

  5. 你应该掌握的C++ RAII手法:Scopegaurd

    C++作为一门Native Langueages,在C++98/03时代,资源管理是个大问题.而内存管理又是其中最大的问题.申请的堆内存需要手动分配和释放,为了确保内存正确释放,一般原则是" ...

  6. C++中的RAII介绍 资源管理

    摘要 RAII技术被认为是C++中管理资源的最佳方法,进一步引申,使用RAII技术也可以实现安全.简洁的状态管理,编写出优雅的异常安全的代码. 资源管理 RAII是C++的发明者Bjarne Stro ...

  7. c++11 实现RAII特性

    参考文章https://blog.csdn.net/pongba/article/details/7911997 什么是RAII 技术?(参见百度百科相关条目) RAII(Resource Acqui ...

  8. C++中的RAII介绍

    摘要 RAII技术被认为是C++中管理资源的最佳方法,进一步引申,使用RAII技术也可以实现安全.简洁的状态管理,编写出优雅的异常安全的代码. 资源管理 RAII是C++的发明者Bjarne Stro ...

  9. The RAII Programming Idiom

    https://www.hackcraft.net/raii/ https://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

随机推荐

  1. li的inline-block出现间隙原因,解决方案

    <style type="text/css"> body{ margin:0 0; padding:0 0; font-size: 14; text-decoratio ...

  2. wordpress角色权限汇总

    我们在用wordpress开发的时候有时候需要设置不同的用户组及权限,具体有哪些角色权限呢?随ytkah一起来看看吧.WordPress使用了角色的概念,旨在让站点所有者能够控制用户在站点中可以做什么 ...

  3. 记一次部署时报java.lang.NoSuchMethodError:javax.persistence.spi.PersistenceUnitInfo.getValidationMode()Ljavax / persistence / ValidationMode;的解决办法

    楼主在部署war包的时候,本地启动不报错,服务器商报如下问题: Error creating bean with name 'entityManagerFactory' defined in clas ...

  4. CF1102D-Balanced Ternary String-(贪心)

    http://codeforces.com/problemset/problem/1102/D 题意: 有n个字符,只能为012,现要通过变换让012的数量相等,并且使字典序最小. 解题: 由样例可以 ...

  5. class [org.springframework.context.annotation.ComponentScanBeanDefinitionParser] are only available on JDK 1.5 and higher

    在搭建SSM项目时报了以下的错误: 06-Oct-2019 11:55:52.109 信息 [RMI TCP Connection(5)-127.0.0.1] org.apache.catalina. ...

  6. ES6学习笔记--Object.is()

    ES5比较两个值是否相等, 相等运算符(==)和恒等运算符(===).它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0. javascript缺乏一种运算,在所有环境中, ...

  7. hadoop spark合并小文件

      一.输入文件类型设置为 CombineTextInputFormat hadoop job.setInputFormatClass(CombineTextInputFormat.class) sp ...

  8. luogu_3645: 雅加达的摩天楼

    雅加达的摩天楼 题意描述: 有\(N\)座摩天楼,从左到右依次编号为\(0\)到\(N-1\). 有\(M\)个信息传递员,编号依次为\(0\)到\(M-1\).编号为i的传递员最初在编号为\(B_i ...

  9. s3-sftp-proxy goreleaser rpm &&deb 包制作

    上次写过简单的s3-sftp-proxy基于容器构建以及使用goreleaser构建跨平台二进制文件的,下边演示下关于 rpm&&deb 包的制作,我们只需要简单的配置就可以生成方便安 ...

  10. AntDesign-React与VUE有点不一样,第一篇深入了解React的概念之一:JSX

    AntDesign-React与VUE有点不一样,第一篇深入了解React的概念之一:JSX 一.什么是JSX 使用JSX声明一个变量(REACT当中的元素): const element =< ...