转自 程序喵大人的RAII 妙用之 ScopeExit
以下是正文部分,我对排版和错别字进行了一点修改
什么是 RAII
Resource Acquisition Is Initialization,资源获取即初始化,将资源的生命周期与一个对象的生命周期绑定,举例来说就是,把一些资源封装在类中,在构造函数请求资源,在析构函数中释放资源且绝不抛出异常,而一个对象在生命周期结束时会自动调用析构函数,即资源的生命周期与一个对象的生命周期绑定。
RAII 的应用
见如下代码:
std::mutex mutex;
void func() {}
void NoRAII() {
mutex.lock();
func();
if (xxx) {
mutex.unlock();// 多次需要调用 unlock() ,还有可能忘记调用 unlock 导致一直持有锁
return;
}
...
mutex.unlock();
}
void RAII() { // 不需要显式调用 unlock
std::lock_guard<std::mutex> lock(mutex);
func();
if (xxx) {
return;
}
...
return;
}
RAII 的应用非常多,C++ 的 STL 基本都遵循 RAII 规范,典型的如vector, string, lock_guard, unique_lock, shared_ptr, unique_ptr 等,这里不会介绍这些 STL 的使用,相信大家也都会使用,如果有相关需求可以留言。
RAII 的巧用
最近研究了 boost 中的 ScopeExit ,发现这是个很高级的特性,利用 RAII 特性,可以在作用域结束时自动关闭已经打开的资源或做某些清理操作,类似于 unique_ptr,但又比 unique_ptr 方便,不需要自定义 delete 函数。
举例
如果没有 ScopeExit
void test () {
char *test = new char[100];
if (a) {
delete[] test; // count 1
return;
}
xxx;
if (b) {
delete[] test; // count 2
return;
}
...
delete[] test; // count 3
}
使用了 ScopeExit
void test () {
char *test = new char[100];
std::ofstream ofs("test.txt");
ScopeExit {
delete[] test; // 在 test 函数生命周期结束后自动执行 delete[] 操作
ofs.close(); // 在生命周期结束后自动关闭文件,这里只是举个不恰当例子,ofstream 自动生命周期结束后就会关闭
};
if (a) {
return;
}
xxx;
if (b) {
return;
}
...
}
当然,正常 C++ 代码不鼓励使用裸指针,可以使用智能指针来申请资源,这里只是举个例子,使用 ScopeExit 也可以用于处理文件资源的关闭等等。
两者代码比较后优劣程度显而易见,不使用 ScopeExit 需要在 return 前多次做资源清理操作,而使用了 ScopeExit 则只需做一次声明,在作用域结束后会自动进行相关的资源清理操作,方便而且不易出错。
ScopeExit 实现
这里参考 boost 使用 C++11 实现了一套 ScopeExit 机制
class ScopeExit {
public:
ScopeExit() = default;
ScopeExit(const ScopeExit&) = delete; ///< 禁用左值引用构造
void operator=(const ScopeExit&) = delete;
ScopeExit(ScopeExit&&) = default; ///< 默认右值引用构造
ScopeExit& operator=(ScopeExit&&) = default;
/// @note 可变参(构造)函数模板
template <typename F, typename... Args>
ScopeExit(F&& f, Args&&... args) { ///< 右值引用
/// @note 绑定(完美转发地)可调用实体和可变参数
func_ = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
}
~ScopeExit() {
if (func_) {
func_(); ///< 运行可调用实体
}
};
private:
std::function<void()> func_; ///< 封装可调用实体
};
/// @note 通过一系列宏定义构造 ScopeExit 实例
#define _CONCAT(a, b) a##b
#define _MAKE_SCOPE_(line) ScopeExit _CONCAT(defer, line) = [&]()
#undef SCOPE_GUARD
#define SCOPE_GUARD _MAKE_SCOPE_(__LINE__)
使用方式如下:
void test () {
char *test = new char[100];
std::ofstream ofs("test.txt");
SCOPE_GUARD{
delete[] test;
ofs.close();
};
if (a) {
return;
}
...
if (b) {
return;
}
...
}
我的总结【不是原作者】
如下图所示,根据行号命名了 ScopedExit 对象
当 ScopedExit 对象析构的时候会调用 std::function 对象所封装的调用实体。
另外提一嘴,我在研究 AE 插件开发源码时,也发现了 RAII 在管理(AE 自己的)设备上下文和 GL 渲染上下文上的妙用
头文件代码:
/// <summary>
/// 用于保存和恢复(设备和渲染)上下文
/// </summary>
class SaveRestoreOGLContext
{
public:
SaveRestoreOGLContext();
~SaveRestoreOGLContext();
private:
#ifdef AE_OS_MAC
CGLContextObj o_RC;
NSOpenGLContext* pNSOpenGLContext_;
#endif
#ifdef AE_OS_WIN
HDC h_DC; ///< Device context handle - 设备上下文
HGLRC h_RC; ///< Handle to an OpenGL rendering context - OGL 渲染上下文
#endif
SaveRestoreOGLContext(const SaveRestoreOGLContext &);
SaveRestoreOGLContext &operator=(const SaveRestoreOGLContext &);
};
源文件代码:
/// <summary>
/// RAII : 构造函数直接将保存当前的 RC 和 DC
/// </summary>
SaveRestoreOGLContext::SaveRestoreOGLContext()
{
#ifdef AE_OS_WIN
h_RC = wglGetCurrentContext();
h_DC = wglGetCurrentDC();
#endif
#ifdef AE_OS_MAC
ScopedAutoreleasePool pool;
pNSOpenGLContext_ = [NSOpenGLContext currentContext];
o_RC = CGLGetCurrentContext();
#endif
}
/// <summary>
/// RAII : 析构函数直接将先前保存的 RC 和 DC 设置为当前
/// </summary>
SaveRestoreOGLContext::~SaveRestoreOGLContext()
{
#ifdef AE_OS_WIN
if (h_RC != wglGetCurrentContext() || h_DC != wglGetCurrentDC())
{
if (!wglMakeCurrent(h_DC, h_RC))
{
DWORD dwLastErr(GetLastError());
// complain
}
}
#endif
#ifdef AE_OS_MAC
ScopedAutoreleasePool pool;
if (pNSOpenGLContext_ != [NSOpenGLContext currentContext] && pNSOpenGLContext_)
{
[pNSOpenGLContext_ makeCurrentContext];
}
makeCurrentFlush(o_RC);
#endif
}
具体的使用例子
PF_Err err = PF_Err_NONE;
try
{
// always restore back AE's own OGL context
/// @note
/// 保存现场以便恢复 AE 自己的 OGL 上下文【构造即保存,析构即恢复】
SaveRestoreOGLContext oSavedContext;
AEGP_SuiteHandler suites(in_data->pica_basicP);
// Now comes the OpenGL part - OS specific loading to start with
/// @note
/// 构造平台特定的(设备和 OGL)上下文,用于插件的渲染工作
S_GLator_EffectCommonData.reset(new AESDK_OpenGL::AESDK_OpenGL_EffectCommonData());
AESDK_OpenGL_Startup(*S_GLator_EffectCommonData.get());
S_ResourcePath = GetResourcesPath(in_data);
}
catch (PF_Err &thrown_err)
{
err = thrown_err;
}
延申阅读 —— RAII 妙用之计算函数耗时