一个适用于嵌入式的强类型、格式化、模板化的C++打印/日志库
作为一个长期使用 Go/C++ 语言的我来说,总是会抱怨 C++ 里面为什么就是没有 Go 语言里面那样简单明了、易用的各种库。 以致于,我经常会用 Go 的模式来写各种 C++ 库以满足日常使用。
今天所介绍的 printf/log
库就是其中之一。然而看其名字就知道,这不就是一个语句打印/日志输出库?
确实如此,但是哪个写 C++ 的人又没有造过自己的 String 类呢?没有。正是因为标准库的匮乏,所以人们总是不停地造各种自己的轮子🛞,乐此不疲。
可以直接跳转到后面查看示例。
std::printf
和 std::iostream
?
首先,为什么不使用 std::iostream
?
- 仅仅
#include <iostream>
就会使目标程序编译出来的目标文件增加 200KB 左右。这在嵌入式系统/单片机程序上是完全无法接受的。 要知道,我测试的平台CH32V003
仅有 16KB 的空间(Flash),增加 200KB 的大小是绝对无法接受的。 - 使用
<<
来输出其实非常难受:包括格式设置、包括优先级考量。
其次,为什么不用 std::printf
?
- 历史非常古早了,其实现方式也非常 hack。
- 一不小心就会崩溃给你看。
- 参数不支持类型检查,也存在隐式转换。
- 比如把
-1
输出为255
。
- 比如把
- 格式化修饰符需要与参数严格对应,否则输出结果会出错。
- 比如:
%d
,%u
,%ld
,%lu
,%lld
…… - 再比如
inttypes.h
里面的一堆:PRIdN PRIdLEASTN PRIdFASTN PRIdMAX PRIdPTR PRIiN PRIiLEASTN PRIiFASTN PRIiMAX PRIiPTR
纯纯地恶心人…… - 关键是,不同的平台(8位/16位/32位机)上,
int
的大小还不一样,真的我哭死。
- 比如:
- 实现很复杂/成熟,为目标体积增加不少
- 使用
std::printf
的时候,用 objsize 查看,经常会发现,它是占用最大的函数 - 然而在嵌入式设备上,通常不需要这么复杂/完整/成熟的实现,所以市面上出现了各种 printf 的替代实现,比如:mini_printf。
- 使用
总之,用户需要时时刻刻记住格式化修饰符需要与参数的类型对应。心智负担很大。
当然,以上我只是稍微列举了其中几点我认为非常难受的地方,还有很多很多……
Go 语言是如何做的?
Go 里面的 fmt 包大大简化了 C/C++ 的格式控制字符的使用:
- 所有的整数数值类型均可以
%d
来输出:- 不需要区分有符号/无符号
- 不需要区分数据的字长:字节、单字、双字、四字
- 所有的类型均可以用
%v
来输出:- 会寻找值的默认类型来输出
- 比如整数就用
%d
,字符就用%c
,字符串就用%s
- 可以用
%t
输出布尔值:- 你正在使用的 C 语言编译器里面可能没有 bool/_Bool 类型(气死你)
- 直接输出
true
/false
- 直接用
%c
输出字符:- 支持任何 Unicode 码位的字符输出
- 而在 C 里面,只支持单字节字符的输出
- 支持所有实现
Stringer
接口的类型- 这就对自定义类型非常友好
- 不只是适用于普通数据类型(POD)的输出
我的实现
我结合了上述两者的优点来实现了我自己的 printf/log 库。
虽然看起来内部实现大量运用了 C++ 的模板以及元编程,但实际上, 这个库只导出了一个函数供用户使用:
1 2 3 4 5 6 7 8 |
|
剩余的看起来像 printf
的东东,几乎全部是重载(多达 30+)。
支持的类型
基础类型
-
bool
-
char
,unsigned char
,char32_t
-
int
,unsigned int
-
int8_t
,uint8_t
,int16_t
,uint16_t
,int32_t
,uint32_t
,int64_t
,uint64_t
-
const char*
- 任何指针(输出地址)
-
float
,double
扩展类型
- 带
const char* toString()
方法的结构体或类 -
std::pair
输出形如K:V
。字符串不会自动加引号,原样输出。 -
std::map
输出形如 JSON 对象。 -
std::vector
输出形如 JSON 数组。 -
std::unordered_map
输出形如 JSON 对象。
格式支持
-
常规:
- %% 字面值 %
- %v 值的默认格式
-
布尔(%v = %t):
- %t 布尔值:true / false
-
字符(%v = %c):
- %c 对应的 Unicode 符号。
-
整数(%v = %d):
- %b 整数的二进制
- %o 整数的八进制
- %d 整数的十进制
- %x 十六进制(小写)
- %X 十六进制(大写)
- %c Unicode 码点值
-
浮点数:
- %f
-
字符串(%v = %s):
- %s 字符串
- %X
- %x
-
指针(%v = %p):
- %p 0x 表示的十六进制,固定显示为指针长度,大写字母。
-
std::pair
输出格式为:T1:T2
-
std::vector
类似 JSON 的数组:
[1,2,3]
。 -
std::map<K,V>
/std::unordered_map<K,V>
类似 JSON 的对象:
{k1:v1,k2:v2}
。
样式支持
- 颜色:黑色、白色、红色、绿色、黄色、蓝色、紫色、青色。
- 字体:加粗、斜体、下划线、闪烁。
Bugs
可能是 Bugs,也可能是设计如此。
- 不支持类似
%[0]s
的表示(根据 [] 内的数字引用第 N 个参数)。- 因为实现方式使用了 C++ 的模板展开,无法提前和延迟获取到顺序不一致的参数。
- 无解,但是还好,很少被人知道/使用,并且也能非常轻易地绕过限制。
- 多次 printf 调用之间的样式无法嵌套。否则需要递归加状态,实现意义似乎不是很大。
- fmt 字符串不能是
std::string
(参数可以),有意设计成这样。
日志库
日志库只是基于 printf
增加了一些预定义的样式,从而定义了一些形如:logInfo
,logErr
,logDebug
的函数,其使用方式与 printf
并无异。
一些示例
基础类型的输出示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
输出如下:
Literal: %, %
true false
1 10 11 100 101 110 111 1000 1001 1010
1 2 3 4 5 6 7 10 11 12
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 a
1 2 3 4 5 6 7 8 9 A
33
hello stuff %s
Unicode: 桃 🍑 🍌
Unicode: 我
Unicode: � (yes, a question mark)
A 🍑
标准库容器类型/复杂类型作为日志输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
|
看起来很复杂,实则一点也不简单:
Debug INFOABC INFO: Atrue Red: str Warn: str Info: 111, 1, A, end.true Error: true end Str: ABC Array: [[1,2,3],[4,5,6]] [] Map: {0:X000,1:X111} Map+Vector: {A:[1,2,3],B:[4,5,6]} Pair: 1:2 unordered_map: {0:1} set: [1,2,3,4,5,6] array: [1,2,3], 0x000000016F182C68
注:以上 HTML 由 Aes2Htm 命令从终端输出生成:./main | ./aes2htm --html > log.html
。
结语
目前几个月下来的使用体验远比 std::printf
来得舒服。
但是这个库并没有独立出来,而是作为我的嵌入式基础代码玩意儿库的一部分存在于 libstuff/base/log at main · movsb/libstuff 中。 可以一同享用。🤪🤪🤪