你应该掌握的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 对象. ...
随机推荐
- [持续交付实践] pipeline使用:Multibranch Pipeline
前言 在探讨multiBranch Pipeline之前,很有必要先探讨下如何制定有效的代码分支管理规范,使用高效的版本控制系统,并对构建产物及其依赖进行管理.我们首先要强调,需要进行版本控制的不仅是 ...
- 06 python下
# st='hello kitty {name} is {age}' # # print(st.count('l')) # 统计元素个数 # print(st.capitalize()) # 首字母大 ...
- (4)网络配置及CRT远程连接
修改linux虚拟机中某一网卡的网络配置: 打开终端,输入命令vi /etc/sysconfig/network-scripts/ifcfg-eth0 在文件中写入以下内容: (这里有个错误,DNS要 ...
- synchronized 和reentrantlock的优缺点
reentrantlock的优点 可以添加多个检控条件, 如果使用synchronized,则只能使用一个. 使用 reentrant locks 可以有多个wait()/notify() 队列. [ ...
- jQuery.extend 与 jQuery.fn.extend
extend方法为jQuery对象的核心之一,语法如下: jQuery.extend([deep], target, object1, [objectN]),返回值Object. 概述:用一个或多个其 ...
- 深入理解Java中的IO
深入理解Java中的IO 引言: 对程序语言的设计者来说,创建一个好的输入/输出(I/O)系统是一项艰难的任务 < Thinking in Java > 本文的目录视图如下: ...
- Mysql添加新用户遇到的一些小问题
登陆命令:mysql -u root -p 添加本地用户:create user 'sheet'@'localhost' identified by '123456' ; 添加允许外网IP访问的用户 ...
- linux服务器系统负载监控-shell脚本
一.监控服务器系统负载情况: 1.用uptime命令查看当前负载情况(1分钟,5分钟,15分钟平均负载情况) # uptime 15:43:59 up 186 days, 20:04, 1 us ...
- GPL_LGPL
LGPL 与GPL的区别 GPL(GNU General Public License) 我们很熟悉的Linux就是采用了GPL.GPL协议和BSD, Apache Licence等鼓励代码重用的 ...
- react项目的react-router-dom路由的使用
现在测试一下react-router-dom路由的使用,首先在App.js这个文件搭配路由 import React, { Component } from 'react'; import {Link ...