這次把和日志相關的其他東西一並說了。
一、vaformat
C++日志接口通常有兩種形式:流輸入形式,printf形式。
我采用printf形式,因為流輸入不好控制格式。
printf形式要求日志接口支持不定長參數,我沒有直接在日志實現類裡邊支持不定長參數,而是只接受一個字符串參數,可以參見第一篇。
為什麼呢?
如果要成為不定長參數,就是這樣
bool log_string(const LOG_LEVEL level, const char* file, const int line, const char *s, ...);
那麼在每一個log_xxx的變體裡就都要寫_vsnprintf_s那一套代碼了,而且是完全一樣的(我不知道__VA_ARGS__宏是否可以傳遞),這顯然是不好的做法。
我把不定長參數的處理放在了宏定義裡,類似:
#define ErrorLog(s, ...) _Log(LOG_ERROR, __FILE__, __LINE__, vaformat(MAX_LOG_BUFFER, s, __VA_ARGS__))
vaformat就是處理不定長參數的:
std::string vaformat(const size_t max_size, const char* msg, ...); std::wstring vaformat(const size_t max_size, const wchar_t* wmsg, ...);
因為並不知道格式化後有多長,所以要指定最大長度,如果格式化後的長度大於最大長度,則截斷。vaformat裡還有一個小技巧,當指定的max_size小於1024的時候,使用棧空間,否則申請堆內存,這是從std::string的實現中學來的——SSO短字符串優化。
可以看下vaformat的實現,兩個版本的代碼基本一樣,這樣當然是不好的,但是我不知道怎樣把他們合並起來,這是一個todo。類似的問題下面還有。
二、CLastErrorFormat
這個東西是用來解決第一篇裡提到的記錄LastErrorCode的問題的。
它的主要功能就是把error code轉換成文本描述,合適的構造也可以省去GetLastError的調用:
class CLastErrorFormat : public boost::noncopyable { public: CLastErrorFormat() : m_code(GetLastError()) { } CLastErrorFormat(const DWORD code) : m_code(code) { } ~CLastErrorFormat() { } public: const DWORD code() const { return m_code; } const std::string& str() { //... } const std::wstring& wstr() { //... } private: //... };
日志實現類裡對應的接口:
bool log_last_error(const LOG_LEVEL level, const char* file, const int line, CLastErrorFormat& e, const std::string& prefix);
接受一個CLastErrorFormat的引用,然後在記錄日志的時候,把error code和其對應的描述也記錄下來:xxx, error code: 999, error msg: yyy
最終的日志接口是有兩個版本的:一個接受一個CLastErrorFormat參數;另一個省去,在函數內部自己構造。
三、str_encode
我不可能在日志文件裡一會記寬字符串,一會記窄字符串,那就沒法看了,又考慮到日志文件的大小,我最終決定,按照窄字符串SystemCurrentCodePage(在簡體中文版的Windows上,就是GB2312)編碼記錄日志,所以對於寬字符串我還要轉換成窄字符串。
Windows提供了兩個API來做編碼轉換:MultiByteToWideChar和WideCharToMultiByte,而這兩個API總是要兩次調用才能安全的轉換。我將其稍稍封裝了一下,做成了兩個函數:
std::wstring multistr2widestr(const unsigned int from_code_page, const std::string& s); std::string widestr2multistr(const unsigned int to_code_page, const std::wstring& ws, const char *default_char = NULL);
注:SystemCurrentCodePage的代碼頁編碼就是CP_ACP。
這樣在記寬字符串的時候總是會慢一些,所以我代碼中,能用窄字符串的地方我都用窄字符串了。
四、any_lexical_cast
代碼中總是免不了要做類型轉換,特別是把數字轉換成字符串,為了簡單一點,我使用了boost的lexical_cast,雖然大家都說這貨效率低,因為使用了C++的流,但是我堅持“先正確,再優化”的原則,還是使用了它。
然而,這個東西使用起來有兩處不便:
1. 轉換失敗的時候會拋異常
2. 把bool轉換成string的時候是0或1,不是true或false
為了解決這兩個問題,我又做了一下封裝:
1. 轉換失敗的時候,填充為默認值。調用者必須提供默認值
2. 特化對於bool和string之間的轉換
這就是any_lexical_cast:
template<typename Target, typename Source> Target any_lexical_cast(const Source& src, const Target& fail_value) { Target value = fail_value; try { value = boost::lexical_cast<Target>(src); } catch (boost::bad_lexical_cast&) { value = fail_value; } return value; } template<> bool any_lexical_cast<bool, std::string>(const std::string& src, const bool& fail_value); template<> bool any_lexical_cast<bool, std::wstring>(const std::wstring& src, const bool& fail_value); template<> std::string any_lexical_cast<std::string, bool>(const bool& src, const std::string&); template<> std::wstring any_lexical_cast<std::wstring, bool>(const bool& src, const std::wstring&);
具體實現請參看源碼。
五、CSelfPath
日志初始化接口通常需要提供一個路徑參數,以指定日志存放路徑。我為其增加了一個默認路徑:當傳遞空字符串時,將日志文件放在應用程序所在路徑的log目錄下,若log目錄不存在,則先創建。
獲取應用程序所在路徑本可以放在日志模塊內部,但考慮到別的地方可能也會用到,而且應用程序一旦啟動,路徑就不會變,所以就做成了一個單例類CSelfPath。
CSelfPath僅在構造函數中調用GetModuleFileNameA一次獲取路徑並分割成目錄、文件名等等部分。
六、CLoggerImpl與Logger
日志的實現類裡邊有好多東西我都不想給調用者看到,典型如private的成員;還有日志實現類的接口並不易用。所以我在日志實現類和調用者之間又引入了一個間接層Logger,它的主要作用就是隱藏日志實現類和使接口更“親民”。當然除了這個我還給了它一些別的功能:控制日志輸出級別。Logger並不是一個類。
七、Disable 3rd party library warning
我在使用boost關於string的algorithm的時候,發現編譯器會大段的警告,這來自boost庫中對std::copy的使用,而我明確的知道boost庫的這段代碼是正確的。這些警告又多又煩人,有沒有安全的辦法消除這個警告?
肯定有了:
#pragma warning(push) #pragma warning(disable:4996) #include <boost/algorithm/string.hpp> #pragma warning(pop)
上面的代碼保存為一個頭文件:boost_algorithm_string.h。以後要包含boost/algorithm/string.hpp時,均以boost_algorithm_string.h代替。
源碼:https://git.oschina.net/mkdym/DaemonSvc.git (主)&& https://github.com/mkdym/DaemonSvc.git (提升逼格用的)。
2015年11月1日星期日