【C/C++】GNUC實(shí)現(xiàn)日志庫
C++日志庫,網(wǎng)上也不少了,很多是流式風(fēng)格的:
這種風(fēng)格在復(fù)雜日志的時(shí)候,代碼有一點(diǎn)亂:
很多人更喜歡C的風(fēng)格,清晰緊湊:
然而,C語言中實(shí)現(xiàn)這種類似printf的函數(shù),可能會(huì)導(dǎo)致格式和參數(shù)不匹配的UB,C++則可利用重載和模板來實(shí)現(xiàn)參數(shù)接收、封裝,進(jìn)一步用運(yùn)行時(shí)檢查,我之前寫過一個(gè)SmartPrintf(https://www.bilibili.com/read/cv18033957),但是代碼復(fù)雜且有運(yùn)行時(shí)消耗
作為標(biāo)準(zhǔn)庫的函數(shù),printf是有特殊優(yōu)待的,如果fmt字段是字符串字面量,則大多數(shù)現(xiàn)代編譯器會(huì)進(jìn)行檢查,在格式和數(shù)據(jù)類型不匹配時(shí)報(bào)警:
但日志庫是我們自己實(shí)現(xiàn)的,就沒這個(gè)待遇了:
編譯器沒檢查出來錯(cuò)誤,只能運(yùn)行時(shí)出問題了
能讓它也有這樣的待遇嗎?GNUC提供了擴(kuò)展屬性來支持:
GNUC擴(kuò)展中,我們可以用__attribute__給各種語法元素(函數(shù)、類型、語句……等等)添加屬性修飾,來告訴編譯器一些信息,從而擴(kuò)展它的行為
在這里,函數(shù)log的定義中使用了屬性修飾,修飾類型是format,內(nèi)容則是告訴編譯器,這個(gè)log函數(shù)的簽名是類似printf的樣式(其實(shí)就是要求編譯器做個(gè)檢查),format的第一個(gè)參數(shù)指明樣式(printf),第二個(gè)參數(shù)是函數(shù)簽名中fmt的位置(log的第1個(gè)參數(shù)),第三個(gè)參數(shù)是指明可變參數(shù)域從第幾個(gè)參數(shù)開始(log的第2個(gè)參數(shù)位置),這樣一來,編譯器就能獲取到對(duì)應(yīng)的信息,從而做和標(biāo)準(zhǔn)庫一樣的檢查了
和printf類似的標(biāo)準(zhǔn)庫接口,也都有對(duì)應(yīng)的檢查屬性,例如scanf、strftime等和格式有關(guān)的,編譯器都可以幫你檢查fmt串,比如說還是上面的log函數(shù),我們要給它加上一個(gè)時(shí)間打印,但又希望能自定義時(shí)間格式,就可以:
這里可以簡單通過set_time_fmt函數(shù),來設(shè)置全局變量time_fmt為自定義的時(shí)間格式,同樣的道理,這個(gè)函數(shù)通過屬性修飾,可以讓編譯器幫忙檢查設(shè)置的fmt是不是strftime需要的格式(當(dāng)然,這個(gè)fmt也必須通過字符串字面量來設(shè)置,否則沒辦法檢查)
回到日志內(nèi)容打印,上面的代碼中我們看到,log的簽名和printf是一樣的,含義也一樣,而且下層實(shí)際也是調(diào)用printf來打印,為了處理可變參數(shù)“...”域,標(biāo)準(zhǔn)C必須用到va_arg,而GNUC在這里提供了更簡單的做法,有一個(gè)內(nèi)建函數(shù)可以直接傳遞可變參數(shù)域:
__builtin_va_arg_pack這個(gè)內(nèi)建函數(shù)的“調(diào)用”,就像是把當(dāng)前函數(shù)的可變參數(shù)“...”直接寫在這里一樣,這樣就省的用va_arg了,顯然就像我之前文章講過的,這種“內(nèi)建函數(shù)”并不是一個(gè)真正的函數(shù),所以這里也不是真正的“調(diào)用”,只是給編譯器看的,類似宏替換的一個(gè)代碼標(biāo)記
因此要使用這個(gè)內(nèi)建函數(shù),編譯器必須同時(shí)知道調(diào)用者調(diào)用時(shí)的輸入,所以這個(gè)函數(shù)必須是強(qiáng)制內(nèi)聯(lián)(上面用always_inline屬性修飾),且因?yàn)槭菑?qiáng)制內(nèi)聯(lián),其實(shí)現(xiàn)也必須被使用者可見(也就是說,一般得寫到頭文件中)。由于這些限制,可變參數(shù)的這種直接透?jìng)鞯姆绞?,一般只用于相關(guān)接口的簡單封裝中,避免使用va_args的麻煩
對(duì)于一個(gè)log來說,還有一個(gè)重要的內(nèi)容是日志打印的代碼位置,如果要結(jié)合可變參數(shù)域,那么這個(gè)就只能通過宏來實(shí)現(xiàn)了,可以用標(biāo)準(zhǔn)的__FILE__和__LINE__宏,但既然是講GNUC,最推薦的還是對(duì)應(yīng)的內(nèi)建函數(shù):