【C/C++】GNUC实现日志库
C++日志库,网上也不少了,很多是流式风格的:
这种风格在复杂日志的时候,代码有一点乱:
很多人更喜欢C的风格,清晰紧凑:
然而,C语言中实现这种类似printf的函数,可能会导致格式和参数不匹配的UB,C++则可利用重载和模板来实现参数接收、封装,进一步用运行时检查,我之前写过一个SmartPrintf(https://www.bilibili.com/read/cv18033957),但是代码复杂且有运行时消耗
作为标准库的函数,printf是有特殊优待的,如果fmt字段是字符串字面量,则大多数现代编译器会进行检查,在格式和数据类型不匹配时报警:
但日志库是我们自己实现的,就没这个待遇了:
编译器没检查出来错误,只能运行时出问题了
能让它也有这样的待遇吗?GNUC提供了扩展属性来支持:
GNUC扩展中,我们可以用__attribute__给各种语法元素(函数、类型、语句……等等)添加属性修饰,来告诉编译器一些信息,从而扩展它的行为
在这里,函数log的定义中使用了属性修饰,修饰类型是format,内容则是告诉编译器,这个log函数的签名是类似printf的样式(其实就是要求编译器做个检查),format的第一个参数指明样式(printf),第二个参数是函数签名中fmt的位置(log的第1个参数),第三个参数是指明可变参数域从第几个参数开始(log的第2个参数位置),这样一来,编译器就能获取到对应的信息,从而做和标准库一样的检查了
和printf类似的标准库接口,也都有对应的检查属性,例如scanf、strftime等和格式有关的,编译器都可以帮你检查fmt串,比如说还是上面的log函数,我们要给它加上一个时间打印,但又希望能自定义时间格式,就可以:
这里可以简单通过set_time_fmt函数,来设置全局变量time_fmt为自定义的时间格式,同样的道理,这个函数通过属性修饰,可以让编译器帮忙检查设置的fmt是不是strftime需要的格式(当然,这个fmt也必须通过字符串字面量来设置,否则没办法检查)
回到日志内容打印,上面的代码中我们看到,log的签名和printf是一样的,含义也一样,而且下层实际也是调用printf来打印,为了处理可变参数“...”域,标准C必须用到va_arg,而GNUC在这里提供了更简单的做法,有一个内建函数可以直接传递可变参数域:
__builtin_va_arg_pack这个内建函数的“调用”,就像是把当前函数的可变参数“...”直接写在这里一样,这样就省的用va_arg了,显然就像我之前文章讲过的,这种“内建函数”并不是一个真正的函数,所以这里也不是真正的“调用”,只是给编译器看的,类似宏替换的一个代码标记
因此要使用这个内建函数,编译器必须同时知道调用者调用时的输入,所以这个函数必须是强制内联(上面用always_inline属性修饰),且因为是强制内联,其实现也必须被使用者可见(也就是说,一般得写到头文件中)。由于这些限制,可变参数的这种直接透传的方式,一般只用于相关接口的简单封装中,避免使用va_args的麻烦
对于一个log来说,还有一个重要的内容是日志打印的代码位置,如果要结合可变参数域,那么这个就只能通过宏来实现了,可以用标准的__FILE__和__LINE__宏,但既然是讲GNUC,最推荐的还是对应的内建函数: