你应该掌握的C++ RAII手法:Scopegaurd
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源码实现的,请在评论里留下邮箱。
你应该掌握的C++ RAII手法:Scopegaurd的更多相关文章
- RAII手法封装互斥锁
RAII手法是 Resource Acquisition is Initialization 的缩写,意为“资源获取即初始化”,在使用智能指针时也使用,下面是针对互斥量时的实现, #include & ...
- RAII手法封装相互排斥锁
CriticalSectionWrapper是一个接口类 class CriticalSectionWrapper { public: // Factory method, constructor d ...
- C++ RAII手法实例,不使用智能指针
/* * ===================================================================================== * * Filen ...
- C++中的RAII技法
Resource Acquisition Is Initialization or RAII, is a C++ programming technique which binds the life ...
- C++11 auto_ptr 的问题
auto_ptr作为最早的智能指针,可以实现以RAII手法管理堆区对象,但它设计的本意只是简单的利用C++对于栈区对象的自动析构管理堆区对象, 并不像shared_ptr那样包含引用计数,可以在每次拷 ...
- C++ stringstream介绍,使用方法与例子
From: http://www.usidcbbs.com/read-htm-tid-1898.html C++引入了ostringstream.istringstream.stringstream这 ...
- 写一个Windows上的守护进程(5)文件系统重定向
写一个Windows上的守护进程(5)文件系统重定向 在Windows上经常操作文件或注册表的同学可能知道,有"文件系统/注册表重定向"这么一回事.大致来说就是32位程序在64位的 ...
- 写一个Windows上的守护进程(3)句柄的管理
写一个Windows上的守护进程(3)句柄的管理 在Windows中编程,跟HANDLE打交道是家常便饭.为了防止忘记CloseHandle,我都是使用do-while-false手法: void f ...
- 《Linux多线程服务端编程》笔记——线程同步精要
并发编程基本模型 message passing和shared memory. 线程同步的四项原则 尽量最低限度地共享对象,减少需要同步的场合.如果确实需要,优先考虑共享 immutable 对象. ...
随机推荐
- scrapy-pipeline的方法
scrapy中多个pipeline作用: 一个项目可能需要爬取多个网站,根据每个网站的数据量(处理方式)不同,可创建多个管道 pipeline class SpideranythingPipeline ...
- 微信小程序版本管理
备忘录:<需要修改完善> 打开小程序开发者工具,点击版本管理,点击设置 , 通用更改码云的名字和邮箱 网络认证选择用户名和密码 把密码输入了,点击远程 添加 输入码云的https地址 和仓 ...
- C++学习基础十六-- 函数学习笔记
C++ Primer 第七章-函数学习笔记 一步一个脚印.循序渐进的学习. 一.参数传递 每次调用函数时,都会重新创建函数所有的形参,此时所传递的实参将会初始化对应的形参. 「如果形参是非引用类型,则 ...
- Dubbo注册Zookepper服务的虚拟IP
使用dubbo在zookepper上注册服务,使用dubbo的服务器IP为192.168.70.105 而在zookepper上显示服务提供者为 dubbo://202.102.110.203:808 ...
- 李清华201772020113《面向对象程序设计(java)》第二周学习总结
李清华201772020113<面向对象程序设计(java)>第二周学习总结 第一部分 理论知识 第三章 本章主要讲了java基本知识中的标识符,关键字,注释,以及数据类型,变量,运算符, ...
- leetcode4
public class Solution { public double FindMedianSortedArrays(int[] nums1, int[] nums2) { var nums = ...
- JS判断手机端是否安装某应用
方法一(网页上判断) if (navigator.userAgent.match(/(iPhone|iPod|iPad);?/i)) { var loadDateTime = new Date() ...
- git-如何不写注释能自动带上修改文件信息
背景:每次提交git,都要写注释,有些情况注释不太好写,或者根本没有必要写,这时可以通过自动加注释方法,比如可以追加修改了哪些文件 解决:通过shell脚本,在脚本里面写git命令,add commi ...
- jquery 设置某div里面的内容为此div里面非img标签的内容
$('#div_1').html($('#div_1').children().not("img")); 要注意 <div id="#div_1"> ...
- Python练习-迭代-2018.11.28
#遍历list L=['a','b','c','d'] l=[] a=0 for n in L: l.insert(a,n) a=a+1 print(l) #遍历dict里的key,导出为list L ...