在实际的应用开发中,log模块设计是必不可少的一部分,log模块设计的好坏直接影响到系统的性能和日后的维护。总的来说,log模块在功能上除了日志级别、时间和消息正文这些必须的信息外,最好还能记录日志产生时尽可能多的信息,比如线程ID、模块名称、源文件、代码行等。在log接口的设计上应该尽可能简化,以方便调用。一个调用起来很麻烦的接口,估计是没人想用的。
在基于C/C++开发的应用中,log接口通常设计成带可变参数的C风格函数,像这样: void __cdecl PrintLog(int log_level, const char* format, ...); 对C 程序员这是很合理的选择。但是对于强调强类型安全的C++来说,可变参函数的类型不安全特性是最遭C++程序员诟病之处,这也是C++用 iostream代替printf类函数的主要原因。但是令人奇怪的是,很久以来,我好像没见到过基于C++的类型安全的log接口设计。本文给出了一种基于C++ iostream的类型安全且线程安全的log接口。 类似于printf的其他iostream替代者,我们希望log接口能够这样用,通过流操作符(<<)的重载来解决类型安全问题的同时提供一致性接口: log << "test for iostream log"; log << "fopen failed. errno=" << errno; 但是对于log来说,用iostream的一个问题在于如何界定一条完整的log信息。用可变参的printf型接口的时候,不管输入参数有多少,一条log信息产生一个函数调用,这意味着一个函数调用就是一条完整的信息。而iostream的每一个输出(<<)流操作都是一个函数调用,这就使得界定一条完整的log信息变得困难。 同时,log level和log time也是log信息的必要组成部分,必须要记录。如果接口弄成这样: log << loglvl_info << log_time << "test for iostream log"; log << loglvl_err << log_time << "fopen failed. errno=" << errno; 估计没有谁喜欢用。还有一个要注意的是多线程安全问题,由于一条完整的log信息可能由多个流操作完成,在栈上构造信息变得不可行,因此存放log信息的buffer要在多个流操作(调用)中使用,这可能带来线程安全问题。如果用简单的加锁解决的话,在构造log信息期间就加锁无疑会对整个软件系统造成很大的性能影响。
这些问题中,最关键的是第一个问题,即如何从多个流操作中界定一条完整的log信息。为解决这个问题,我利用了C++的临时对象在表达式结束时析构这一事实。我们可以这样设计logstream接口: class logstream : public std::ostream ...{ public: explicit logstream(); ~logstream() ...{ write_log(m_szLogText); } //... }; 要产生log时这样调用: logstream() << "fopen failed. errno=" << errno; 在这条语句结束时,logstream()临时变量被析构,这样在析构函数中我们就得到了一条完整的log信息,log time也可以在这时产生。而且由于logstream()只是一个局部的临时变量,不会有线程安全问题,没必要用锁。log level则可以在logstream构造的时候记录,改进class logstream如下: #define LL_ERROR 1 #define LL_INFO 2
class logstream : public std::ostream ...{ public: explicit logstream( int level); ~logstream() ...{ write_log(m_szLogText); } }; #define logerr logstream( LL_ERROR) #define loginfo logstream( LL_INFO) 这样调用log接口: logerr << "fopen failed. errno=" << errno; 上面这行其实有一个问题:在C++标准草案中,临时变量是r-value,不能有non-const引用。因此在VC .NET 7.1中你会看到 loginfo << "X"调用的是成员操作符ostream::operator<<(const void*),而不是非成员的operator<<(ostream&,const char*),这导致loginfo << "X"输出一个内存地址而非期望的字符串。 我这里的work-around利用了r-value可以调用成员函数,而成员函数可以返回non-const引用这一事实: class logstream : public std::ostream ...{ public: explicit logstream( int level); ~logstream(); logstream& l_value() ...{ return *this; } }; #define logerr logstream( LL_ERROR).l_value() #define loginfo logstream( LL_INFO).l_value() 关于临时变量的一个比较详细的讨论可以看这里。另外一个要注意的问题是要保证析构函数中的所有操作都不能抛出异常。 附录给出了一份比较完整的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 #include #include #include "logleveldef.h"
using std::ostream; // 为了应付vc6.0的bug
class logstream : public std::ostream ...{ logstream( const logstream& ); const logstream& operator=( const logstream& ); public: explicit logstream( int level, int line, const char* file, const char* function ); ~logstream(); logstream& l_value() ...{ return *this; }
// strstreambuf标准接口 std::strstreambuf *rdbuf() const ...{return ( (std::strstreambuf *)&streambuf_ ); } void freeze(bool f = true) ...{streambuf_.freeze(f); } char *str() ...{return (streambuf_.str()); } std::streamsize pcount() const ...{return (streambuf_.pcount()); }
private: std::strstreambuf streambuf_; LogMessage message; };
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()中不能抛出异常,因为在析构函数中抛出 // 异常是不安全的。具体讨论见"More exceptional C++"一书。 //---------------------------------------------------------- const char* ptr = this->str(); this->freeze( false ); message.write( ptr, this->pcount() ); // never throw }
// __FILE__, __LINE__是ANSI C标准,__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的析构函数,在析构函数 // 中调用write_log()接口写日志 //--------------------------------------------------------
//---------------------------------------------------------- // 注意以下定义的log接口都是一些临时变量,C++标准草案中 // 临时变量是r-value,不能有non-const引用, // 因此在VC.NET 7.1中你会看到loginfo << "X"调用的是成员操作符 // ostream::operator<<(const void*),而不是非成员的 // operator<<(ostream&,const char*),这导致loginfo << "X"输出 // 一个内存地址,而非期望的字符串。 // 我这里的解决方法是:由于r-value可以调用成员函数,成员函数 // 可以返回non-const引用,因此这里的log接口是临时变量 // non-const引用。另一种解决方法是这样用: // const_cast( loginfo ) << "X"; // VC 6.0和VC .NET 7.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 logexit logstream( LL_FATALERR, __LINE__, __FILE__, __FUNCTION__ ).l_value()
//-------------------------------------------------------- // 一般错误。某个模块功能错误,但其它部模块不受影响, // 程序还能继续工作。 //-------------------------------------------------------- #define logerr logstream( LL_ERROR, __LINE__, __FILE__, __FUNCTION__ ).l_value()
//-------------------------------------------------------- // 警告信息。需要提醒用户注意的信息,比如对某个接口传递的 // 调用参数不正确,内存溢出,或设备驱动收到一个不能识别的参数。 // 警告和一般错误的区别在于一般错误是程序的逻辑出现问题, // 警告则是程序本身的自我保护,是正确的逻辑 //-------------------------------------------------------- #define logwarn logstream( LL_WARNING, __LINE__, __FILE__, __FUNCTION__ ).l_value()
//-------------------------------------------------------- // 一般信息。报告目前的状态 //-------------------------------------------------------- #define loginfo logstream( LL_INFO, __LINE__, __FILE__, __FUNCTION__ ).l_value()
//-------------------------------------------------------- // 调试信息。有助于程序员调试使用的信息。 //-------------------------------------------------------- #define _logdbg logstream( LL_DBGINFO, __LINE__, __FILE__, __FUNCTION__ ).l_value()
#endif // __LOG_STREAM_H__ |