使用ScopeGuard在运行环境中监测内部变量
smilemac
1. ScopeGuard简介
我们知道,使用结构化异常来书写一个期望有较高可靠性的函数时,尤其这段函数如果有副作用(side effect),那么在执行失败,需要保持资源一致性的时候,琐碎凌乱的try块会使程序可读性很差,并且看上去很丑陋,Petru Marginean和Andrei Alexandrescu所写的ScopeGuard技术在程序发生异常时保持资源一致性方面有很好的效果,其主要原理是利用了异常发生时堆栈会unwind,try块中的所有局部和临时变量会被正常释放,对象的析构函数会得到正常的调用的特点。如果定义一个类,在其析构函数作一些特定的资源一致性保持的工作,并且在try块中预先声明一些这样的对象,那么发生异常时有关资源一致性保持的工作便总会得到执行。
ScopeGuard包括以下一些类和函数:
class ScopeGuardImplBase:提供监测类的基类,你可以从此基类派生若干可处理不同数量参数的派生类,值得注意的是为不损失效率,此基类的析构函数不是虚函数,作者使用了一个很有趣的技巧来获得多态性。
class ScopeGuardImpl1:这是一个处理一个参数的派生类,用户可以仿照此形式实现多个参数的监测类。
void ScopeGuardImplBase::Dismiss() const throw(): 这是取消监测的函数,如果直到函数返回时都不调用,则动作总会执行。
还有一个辅助类和辅助类型定义:
template <typename Fun, typename Parm>
ScopeGuardImpl1<Fun, Parm>
MakeGuard(const Fun& fun, const Parm& parm) {
return ScopeGuardImpl1<Fun, Parm>(fun, parm)
}
第一个参数表示发生stack unwind时期望被执行的动作,第二个表示动作所需的参数。
typedef const ScopeGuardImplBase& ScopeGuard; //不要小看此定义,此定义是获得多态性的关键
另外还有一个处理引用参数的辅助类和函数:
template <class T> class RefHolder.
template <class T> inline RefHolder<T> ByRef(T& t)
详细内容请参见http://www.cuj.com/documents/s=8000/cujcexp1812alexandr/。
2. 运行时监测的问题
我们知道,有两种程序是很难调试的,一种是动态特征不确定的并行程序,如多线程程序,运行时进程状态与调试时有很大不同,另外一种是与时间密切相关的程序,如实时系统,也是不便于调试的。对于这两种系统,主要的变量监测的手段便是在适当地方将变量值写入日志文件或其他输出设备。但是对于发生异常时的情况怎么办呢,传统的方法是用catch捕获异常,然后将变量写入日志,如下所示:
void f(void)
{
int state1_, state2_;
try {
do something;
} catch(...) {
Log("state1=0x%x\n", state1_);
Log("state2=0x%x\n", state2_);
}
}
显而易见,这种做法有很多缺点:
首先是程序结构性不好,比较难看。
其次是变量必须在try块外面定义,否则只能通过异常变量传递,当有很多变量需要监测时,这种做法是不现实的。
有没有更好的办法呢?答案是有的,就在ScopeGuard中。
3. 使用ScopeGuard监测局部变量
(为什么题目要叫做监测局部变量,先等一会儿,原因后面会介绍。)
因为我们的Log函数需要至少处理两个参数,一个是字符串表示变量名,另外一个是变量的值,因此需要实现一个可处理两个参数的ScopeGuardImp类,这个读者可以仿照Petru的程序自己实现,本文不赘述了。本文介绍另外一种方法。
先实现一个MyLog的函数对象,如下
template<typename F, typename V>
class CMyLog {
F format_;
V value_;
public:
CMyLog(const F& format) : format_(format){};
~CMyLog(void){};
void operator ()(V value){
Log(format_, value);
}
private:
CMyLog();
};
然后再定义一个辅助函数:
template<typename V>
inline CMyLog<string, V> WriteLog(const string& format, const V& value)
{
return CMyLog<string, V>(format);
};
现在就用上面的函数以及SafeGuard来实现监测。
void f(void)
{
int state1_, state2_;
ScopeGuard guard1 = MakeGuard(WriteLog("state1=0x%x\n", state1_), ByRef(state1_));
ScopeGuard guard2 = MakeGuard(WriteLog("state2=0x%x\n", state2_), ByRef(state2_));
do something;
guard1.Dismiss();
guard2.Dismiss();
}
好了,写了这么多,现在终于可以以统一优雅的方式书写监测内部变量的程序了,但是等等,细心的读者可能已经发现,用这段代码监测全局或静态变量没有问题,但是监测局部自动变量也没问题吗?由于编译程序的优化,stack unwind时,guard1难道一定先于state1_释放(destroy)吗?答案是肯定的,这一点已经由C++标准作了保证,C++标准规定,临时变量的释放(destroy)将按照与创建相反的次序执行,并且即使有其他自动和静态变量时,临时变量也保持与这些自动和静态变量的创建相反的次序释放。注意,MakeGuard产生的是一个临时变量,析构时发生作用的是此临时变量。所以,上面的析构次序是:guard2(绑定的临时对象),guard1(绑定的临时对象),state1_和state2_.
这一点对于监测那些相关的状态变量时尤其有用。
4.结论
本文介绍了SafeGuard在编写故障诊断代码方面的一种应用,以这种方式编写的监测代码避免了传统直接使用try...catch结构时代码臃肿、结构性差等毛病,完成的监测代码在软件发行版中也很容易用条件编译语句隔离,不影响程序的整洁,另外有关的辅助类和函数也有比较好的再用性(reusibility)。笔者认为,由于上述优点,此种方法具有较好的使用价值。
5.参考文献
Andrei Alexandrescu and Petru Marginean,“Change the Way You Write Exception-Safe Code — Forever”,http://www.cuj.com/documents/s=8000/cujcexp1812alexandr
<完>