C++作为一门Native Langueages,在C++98/03时代,资源管理是个大问题。而内存管理又是其中最大的问题。申请的堆内存需要手动分配和释放,为了确保内存正确释放,一般原则是"谁分配谁负责释放",但软件工程的复杂性、程序员的编码水平参差不齐等仍然导致内存泄漏、空悬指针等问题。严重的内存泄漏可能很快导致服务器内存耗光而运行崩溃。
托管语言(JAVA、C#、C++ CLI等)为了解决这种问题引入了GC,把内存管理交给机器处理。而C++的解决办法一个是重启程序(内存泄漏严重时),一个就是今天的主角RAII(防止内存泄漏)。
Bjane最早提出RAII理念。 RAII全称(Resource Acquisition is Initialization),即对象构造时所需资源应在构造函数中初始化,对象析构时释放这些资源。这种范式意味着应该用类来封装和管理资源。现代C++提供的智能指针,正是用于实现RAII手法。智能指针是存储指向动态分配对象指针的类,用于生命期控制,能够确保智能指针离开作用域时,自动正确地销毁动态分配地对象,防止内存泄漏。正确地使用智能指针后,理论上程序中应该不会再出现delete,也不用担心内存泄漏问题了。毫不夸张地说,RAII是C++十几年积淀下来的真正好的部分之一。
RAII思想的一个技术应用就是scopeguard。
关于scopeguard技术的讨论见stackoverflow:
Does ScopeGuard use really lead to better code?
某老外激情答复:
If there is one single piece of C++ code that I could recommend every C++ programmer spend 10 minutes learning, it is ScopeGuard (now part of the freely available Loki library).
I decided to try using a (slightly modified) version of ScopeGuard for a smallish Win32 GUI program I was working on. Win32 as you may know has many different types of resources that need to be closed in different ways (e.g. kernel handles are usually closed with CloseHandle()
, GDI BeginPaint()
needs to be paired with EndPaint()
, etc.) I used ScopeGuard with all these resources, and also for allocating working buffers with new
(e.g. for character set conversions to/from Unicode).
What amazed me was how much shorter the program was. Basically, it's a win-win: your code gets shorter and more robust at the same time. Future code changes can't leak anything. They just can't. How cool is that?
大意就是使用loki库的scopeguard后,再也不会忘了释放资源了,大家赶快用。scopeguard的原理是当栈变量离开作用域时自动调用其析构函数,析构函数中调用用户编写的自定义释放资源代码。以windows操作系统为例,存在大量创建内核句柄的API,比如socket或beginpaint等,创建后立马调用scopeguard提供的方法,在里面closehandle或endpaint。
例如下面的代码,
用法1;
用法2:
输出结果:
清理资源代码紧跟着创建句柄代码,看着很清晰,而且会自动清理,用起来非常酷。下面从代码层面来剖析其实现原理。
SCOPE_GUARD_VARIABLES_AUTO_RUNNING_ON_EXIT宏的实现
#ifndef ANONYMOUS_VARIABLES
#define ANONYMOUS_VARIABLES_IMPL(s1, s2) s1##s2
#define ANONYMOUS_VARIABLES(str) ANONYMOUS_VARIABLES_IMPL(str, __LINE__)
#endif
#ifndef SCOPE_GUARD_VARIABLES_AUTO_RUNNING_ON_EXIT
#define SCOPE_GUARD_VARIABLES_AUTO_RUNNING_ON_EXIT \
auto ANONYMOUS_VARIABLES(SCOPE_GUARD_EXIT_STATE) \
= ScopeGuardOnExit () + [&]() throw()//noexcept VS2013不支持noexcept
注意这里的_LINE_宏,作用是定义一个变量,变量名字后缀追加当前代码行号的数字。当用户使用下面的代码时
SCOPE_GUARD_VARIABLES_AUTO_RUNNING_ON_EXIT{
printf("funcExcept:socket leave scope\n");
closesocket(hAcptSock);
};
编译期展开为
auto SCOPE_GUARD_EXIT_STATE13 = ScopeGuardOnExit () + [&]() throw(){
printf("funcExcept:socket leave scope\n");
closesocket(hAcptSock);
};
蓝字里的13是行号,蓝字的+是个重载的全局运算符,其定义如下
template <typename FuncT>
ScopeGuardImpl<typename std::decay<FuncT>::type>
operator+ (ScopeGuardOnExit, FuncT&& func) {
return ScopeGuardImpl<typename std::decay<FuncT>::type>(
std::forward<FuncT>(func));
}
右侧的lanmba函数被作为参数传递为func,返回的是ScopeGuardImpl模板类的实例。这个实例SCOPE_GUARD_EXIT_STATE13是栈变量,其构造时,把lanmba函数带入。
explicit ScopeGuardImpl(FuncT&& func)
: function_(std::move(func))
{
printf("ScopeGuardImpl(FuncT&& func) called");
}
当其销毁时,调用自己的析构函数时,进而调用lambda函数。
~ScopeGuardImpl()
{
if (!dismissed_) {
function_(); //ScopeGuardImpl析构时调用用户自己的方法
}
}
scopegaurd机制大致上如此。但scopeguard的使用是有局限的,如果代码块里的变量内存是用户在堆中new出来的,离开代码块前,你就得在scopeguard里delete 堆指针。但使用智能指针就不需要delete了,使用更方便。所以结论系统API或第三方库创建的资源使用完需要手写代码释放的,且创建和释放在同一个代码块的,用scopeguard管理。用户new出来的内存,使用智能指针管理。scopeguard+智能指针双重使用可保证彻底解决资源管理和内存管理问题。
另外,不管是loki库的scopeguard还是boost的或者第三方提供的类似技术,都无法保证在程序异常(指程序崩溃,而不是程序行为反常,不符合预期)时还能释放资源,并且也没有意义。程序异常了就关掉退出进程,资源自然就释放了。google家的编程规范禁止使用异常技术,程序异常了就crash,从dump文件中分析错误。
本人深以为然。
需要scopeguard源码实现的,请在评论里留下邮箱。