C++临时变量的另类应用:基于iostream的类型安全的log接口设计

几年前做的一个东西,现在专注于系统底层开发,C++的高级功能用的比较少了,写出来回忆回忆。

在实际的应用开发中,log模块设计是必不可少的一部分,log模块设计的好坏直接影响到系统的性能和日后的维护。总的来说,log模块在功能上除了日志级别、时间和消息正文这些必须的信息外,最好还能记录日志产生时尽可能多的信息,比如线程ID、模块名称、源文件、代码行等。在log接口的设计上应该尽可能简化,以方便调用。一个调用起来很麻烦的接口,估计是没人想用的。
在基于C/C++开发的应用中,log接口通常设计成带可变参数的<wbr></wbr>C风格函数,像这样:
void __cdeclPrintLog( int log_level, const char * format,...);

对C 程序员这是很合理的选择。但是对于强调强类型安全的C++来说<wbr></wbr>,可变参函数的类型不安全特性是最遭C++程序员诟病之处<wbr></wbr>,这也是C++用 iostream代替printf类函数的主要原因<wbr></wbr>。但是令人奇怪的是,很久以来,我好像没见到过基于C+<wbr></wbr>+的类型安全的log接口设计。本文给出了一种基于C++ iostream的类型安全且线程安全的log接口。
类似于printf的其他iostream替代者,我们希望log接口<wbr></wbr>能够这样用,通过流操作符(<<)的重载来解决类型安全问题的同时提供一致性接口:
log << " testforiostreamlog " ;
log
<< " fopenfailed.errno= " << errno;

但是对于log来说,用iostream的一个问题在于如何界定一条完整的log信息<wbr></wbr>。用可变参的printf型接口的时候,不管输入参数有多少,一条log信息产生一个函数调<wbr></wbr>用,这意味着一个函数调用就是一条完整的信息。而iostream的<wbr></wbr>每一个输出(<<)流操作都是一个函数调用,这就使得界定一条完整的log信息变得困难。
同时,log level和log time也是log信息的必要组成部分,必须要记录<wbr></wbr>。如果接口弄成这样:
log << loglvl_info << log_time << " testforiostreamlog " ;
log
<< loglvl_err << log_time << " fopenfailed.errno= " << errno;

估计没有谁喜欢用。还有一个要注意的是<wbr></wbr>多线程安全问题,由于一条完整的log信息可能由多个流操作完成<wbr></wbr>,在栈上构造信息变得不可行,因此存放log信息的buffer要在多个流操作(调用<wbr></wbr>)中使用,这可能带来线程安全问题。如果用简单的加锁解决的话<wbr></wbr>,在构造log信息期间就加锁无疑会对整个软件系统造成很大的性能影响。

这些问题中,最关键的是第一个问题,即如何从多个流操作中界定一条完<wbr></wbr>整的log信息。为解决这个问题,我利用了C++的临时对象在表达<wbr></wbr>式结束时析构这一事实。我们可以这样设计logstream接口:
class logstream: public std::ostream
... {
public:
explicitlogstream();
~logstream()
...{
write_log(m_szLogText);
}

//...
}
;

要产生log时这样调用:
logstream() << " fopenfailed.errno= " << errno;

在这条语句结束时,logstream()临时变量被析构<wbr></wbr>,这样在析构函数中我们就得到了一条完整的log信息,log time也可以在这时产生。而且由于logstream(<wbr></wbr>)只是一个局部的临时变量,不会有线程安全问题,没必要用锁。log level则可以在logstream构造的时候记录,改进class logstream如下:
#define LL_ERROR1
#define LL_INFO2

class logstream: public std::ostream
... {
public:
explicitlogstream(intlevel);
~logstream()
...{
write_log(m_szLogText);
}

}
;
#define logerrlogstream(LL_ERROR)
#define loginfologstream(LL_INFO)

这样调用log接口:
logerr << " fopenfailed.errno= " << errno;

上面这行其实有一个问题:在C++标准草案中,临时变量是r<wbr></wbr>-value,不能有non-const引用。因此在VC .NET 7.1中你会看到 loginfo << "X"调用的是成员操作符ostream::operator<<wbr></wbr><(const void*),而不是非成员的operator<<<wbr></wbr>(ostream&,const char*),这导致loginfo << "X"输出一个内存地址而非期望的字符串。
我这里的work-around利用了r-value可以调用成员函数,而成员函数可以返回non<wbr></wbr>-const引用这一事实:
class logstream: public std::ostream
... {
public:
explicitlogstream(intlevel);
~logstream();
logstream
&l_value()...{return*this;}
}
;
#define logerrlogstream(LL_ERROR).l_value()
#define loginfologstream(LL_INFO).l_value()

关于临时变量的一个比较详细的讨论可以看 这里。另外一个要注意的问题是要保证析构函数中的所有操作都不能抛出异常<wbr></wbr>。
附录给出了一份比较完整的logstream接口代码。由于logstream基于iostream,其接口非常简单易用,继承了iostream的很多优点:如对所有数据类型具有一致性的接口,支持iostream library原先支持的所有数据类型,扩展能力很强-新增数据类型对iostream的重载自动适用于logstream。除此之外,该logstream是线程安全的。对于每条log信息,除了基本的log level和log time之外,logstream还能记录生成每条log 的线程ID、模块名称、源文件代码行、函数名等信息。

附:比较完整的logstream接口部分代码。
#ifndef__LOG_STREAM_H__
#define __LOG_STREAM_H__

#include
< strstream >
#include
< string >
#include
< logmessage.h >
#include
" logleveldef.h "

using std::ostream; // 为了应付vc6.0的bug

class logstream: public std::ostream
... {
logstream(
constlogstream&);
constlogstream&operator=(constlogstream&);
public:
explicitlogstream(intlevel,intline,constchar*file,constchar*function);
~logstream();
logstream
&l_value() ...{return*this;}

//strstreambuf标准接口
std::strstreambuf*rdbuf()const
...{return((std::strstreambuf*)&streambuf_);}
voidfreeze(boolf=true)
...{streambuf_.freeze(f);}
char*str()
...{return(streambuf_.str());}
std::streamsizepcount()
const
...{return(streambuf_.pcount());}

private:
std::strstreambufstreambuf_;
LogMessagemessage;
}
;

inline
logstream::logstream(
int level, int line, const char * file, const char * function)
:ostream(
& streambuf_)
,streambuf_(
& LogMessage::mem_alloc, & LogMessage::mem_free)
,message(level,line,file,function)
... {
}


inline
logstream::
~ logstream()
. {
//----------------------------------------------------------
//logstream.h包含在stdhdrs.h中,因此也包含在所有cpp文件中。
//如果直接调用Log接口需要包含log.h文件。这样使文件的依赖性
//大大提高,如果修改Log接口会导致编译所有cpp文件。
//这里调用全局函数,可以不用包含log.h头文件,
//对log接口的修改不会导致其他cpp文件的重新编译,从而
//降低文件的依赖性提高编译速度。
//在release版本中,可以考虑包含log.h头文件从而直接调用Log接口
//省去一个函数调用。但是另一方面,插件程序不依赖于log.h,
//如果要在插件中使用本文件,就必须做条件编译,更加麻烦。
//因此这里为简明起见,只调用一个全局函数,通过在主程序和
//插件程序里对该函数的不同实现调用ILog接口
//----------------------------------------------------------

//----------------------------------------------------------
//LogMessage::write()中不能抛出异常,因为在析构函数中抛出
//异常是不安全的。具体讨论见"MoreexceptionalC++"一书。
//----------------------------------------------------------
constchar*ptr=this->str();
this->freeze(false);
message.write(ptr,
this->pcount());//neverthrow
}



// __FILE__,__LINE__是ANSIC标准,__FUNCTION__是VC扩展,在vc.net中才有
#ifndef__FUNCTION__
#define __FUNCTION__NULL
#endif // __FUNCTION__

#ifdef_DEBUG

#define logdbg(message)do{_logdbg<<message;}while(0)

#else // NDEBUG

#define logdbg(message)do{}while(0)

#endif // _DEBUG

// --------------------------------------------------------
// 以下define定义了几个临时变量。这些临时变量在语句结束
// 时析构,导致调用logstream<level>的析构函数,在析构函数
// 中调用write_log()接口写日志
// --------------------------------------------------------

// ----------------------------------------------------------
// 注意以下定义的log接口都是一些临时变量,C++标准草案中
// 临时变量是r-value,不能有non-const引用,
// 因此在VC.NET7.1中你会看到loginfo<<"X"调用的是成员操作符
// ostream::operator<<(constvoid*),而不是非成员的
// operator<<(ostream&,constchar*),这导致loginfo<<"X"输出
// 一个内存地址,而非期望的字符串。
// 我这里的解决方法是:由于r-value可以调用成员函数,成员函数
// 可以返回non-const引用,因此这里的log接口是临时变量
// non-const引用。另一种解决方法是这样用:
// const_cast<logstream&>(loginfo)<<"X";
// VC6.0和VC.NET7.0对临时变量缺省选择non-const引用(但不
// 符合C++规范),因此没有这个问题。
// 关于临时变量更详细的讨论参见这里:
// http://groups.google.com/groups?hl=zh-CN &lr=&ie=UTF-8&threadm=77q8ju8cqfg11td4qnn24i9unqp54801in%404ax.com&rnum=1&prev=/groups%3Fselm%3D77q8ju8cqfg11td4qnn24i9unqp54801in%25404ax.com
// ----------------------------------------------------------

// --------------------------------------------------------
// 致命错误。程序即将退出
// --------------------------------------------------------
#define logexitlogstream(LL_FATALERR,__LINE__,__FILE__,__FUNCTION__).l_value()

// --------------------------------------------------------
// 一般错误。某个模块功能错误,但其它部模块不受影响,
// 程序还能继续工作。
// --------------------------------------------------------
#define logerrlogstream(LL_ERROR,__LINE__,__FILE__,__FUNCTION__).l_value()

// --------------------------------------------------------
// 警告信息。需要提醒用户注意的信息,比如对某个接口传递的
// 调用参数不正确,内存溢出,或设备驱动收到一个不能识别的参数。
// 警告和一般错误的区别在于一般错误是程序的逻辑出现问题,
// 警告则是程序本身的自我保护,是正确的逻辑
// --------------------------------------------------------
#define logwarnlogstream(LL_WARNING,__LINE__,__FILE__,__FUNCTION__).l_value()

// --------------------------------------------------------
// 一般信息。报告目前的状态
// --------------------------------------------------------
#define loginfologstream(LL_INFO,__LINE__,__FILE__,__FUNCTION__).l_value()

// --------------------------------------------------------
// 调试信息。有助于程序员调试使用的信息。
// --------------------------------------------------------
#define _logdbglogstream(LL_DBGINFO,__LINE__,__FILE__,__FUNCTION__).l_value()


#endif // __LOG_STREAM_H__
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值