在實際的應用開發中,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__ |