【转载】RAII 妙用之 ScopeExit

361 阅读3分钟

转自 程序喵大人的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 对象

image.png

当 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 妙用之计算函数耗时