如何能够快速、方便的编写打印;如何清晰、准确的定位;如何简单并优雅的实现;最后才能让我们摆脱这样枯燥的、重复的工作?
网上有很多强大的日志类工具,我也都使用过一些,有时候也并没有理想中的方便。今天我想分享给大家的一套我自己的解决方案。
在繁杂的项目中,日志打印必不可少。但是编写打印的工作,有时候是无趣的、繁琐的、浪费精力的。
如何能够快速、方便的编写打印;如何清晰、准确的定位;如何简单并优雅的实现;最后才能让我们摆脱这样枯燥的、重复的工作?
网上有很多强大的日志类工具,我也都使用过一些,有时候也并没有理想中的方便。今天我想分享给大家的一套我自己的解决方案。
C++ 中标准输出方式
对于单个变量输出,可以如下方式:
int delay = 5;
std::cout << "delay:" << delay << std::endl;
对于多变量信息输出则需要如下方式:
#include <iostream>
using namespace std;
int main(int argc, char **argv)
{
char *Name = "Bill";
int Age = 10;
float Score = 86.5;
float Height = 121.3;
float Body_Weight = 36;
std::cout << "Name:" << Name << ", "
<< "Age:" << Age << ", "
<< "Score:" << Score << ", "
<< "Height:" << Height << ", "
<< "Body_Weight:" << Body_Weight << std::endl;
}
问题的提出
大家可以看见,需要添加很多对应变量名的字符串,导致写一次打印,非常耗时间。
我一直在想有没有更好的解决方案,形如 logOut(a, b, c, d);
这样简单方便的输出方式?
最后经过不断探索终于找到了一份这样的解决方案,而且只需要加入一个头文件即可,代码如下:
#include "logger.h"
int main(int argc, char **argv)
{
char *Name = "Bill";
int Age = 10;
float Score = 86.5;
float Height = 121.3;
float Body_Weight = 36;
logDebug(Name, Age, Score, Height, Body_Weight);
}
输出结果:
DEBUG [..\cppdemo\main.cpp@main#11]Name:Bill, Age:10, Score:86.5, Height:121.3, Body_Weight:36
大家可能注意到了,背景图的内容就是这个,还有Info、Warn、Error并对应不同颜色输出。
如何实现
那么对于以上效果,如何才能实现呢?
下面我将带领大家一步一步讲解我的心路历程与解决方案。这样也容易让大家了理解其中原理。
单个变量的实现
究其根本,就是减少对变量名称字符串的输入,但是打印的时候能够对应显示,便于我们分析。
对于单个变量,我很早就想到,用过宏“#”就可以轻松实现。
宏 “#” 的妙用
如果熟悉c++宏的小伙伴,可能知道里面有个“#”的用法,可以将对应的参数变成字符串,效果如下:
#define toStr(x) #x
char *str = toStr(hello);
// 等价于
char *str = "hello";
那么对于单个变量最简单的宏实现方式就是:
#define logs(x) std::cout << #x":" << x << std::endl;
...
int delay = 5;
int other = 3;
logs(delay);
logs(other);
输出结果:
delay:5
other:3
显示文件名、函数、行号
对于想要显示文件位置信息,c++中也有对应的宏,分别是 __FILE__
、 __FUNCTION__
、 __LINE__
;
只需要做如下改动即可。
#define FILE_INFO "[" << __FILE__ << '@' << __FUNCTION__ << '#' << __LINE__ << "]"
#define logs(x) std::cout << FILE_INFO << #x":" << x << std::endl
...
int delay = 5;
int other = 3;
logs(delay);
logs(other);
输出结果:
[..\cppdemo\main.cpp@main#21]delay:5
[..\cppdemo\main.cpp@main#22]other:3
设置打印颜色
大家都知道在Linux使用 ls 命令列出文件列表时,不同的文件类型会用不同的颜色显示。那么如何实现这样带颜色的文本输出呢?
在bash中,通常我们可以使用echo命令加-e选项输出各种颜色的文本,例如:
echo -e "\033[31mRed Text\033[0m"
echo -e "\033[32mGreen Text\033[0m"
echo -e "\033[33mYellow Text\033[0m"
echo -e "\033[34mBlue Text\033[0m"
echo -e "\033[35mMagenta Text\033[0m"
echo -e "\033[36mCyan Text\033[0m"
其中:”\033[31m”、”\033[31m”、”\033[0m”等是ANSI转义序列(ANSI escape code/sequence),它控制文本输出的格式、颜色等。
格式 :
\033[显示方式;字体颜色;背景颜色m 中间是变颜色的内容 \033[0m
其中各个参数意义如下:
字体色 背景色 颜色
---------------------------------------------
30 40 黑色
31 41 红色
32 42 绿色
33 43 黃色
34 44 蓝色
35 45 紫红色
36 46 青蓝色
37 47 白色
显示方式 意义
-----------------------------------
0 终端默认设置
1 高亮显示
4 使用下划线
5 闪烁
7 反白显示
8 不可见
那么添加颜色就可以如下处理:
#define OUT_RED "\033[0;31;1m"
#define OUT_GREEN "\033[0;32;1m"
#define OUT_END "\033[0m"
#define FILE_INFO "[" << __FILE__ << '@' << __FUNCTION__ << '#' << __LINE__ << "]"
#define logRed(x) std::cout << OUT_RED << FILE_INFO << #x":" << x << OUT_END << std::endl
#define logGreen(x) std::cout << OUT_GREEN << FILE_INFO << #x":" << x << OUT_END << std::endl
...
int delay = 5;
int other = 3;
logRed(delay);
logGreen(other);
多个变量的实现
最后一个问题,在多变量的时候,如何输出呢?
对于多变量的输出,就是要弄清楚究竟有多少个变要输出,这样就可以扩展宏,进行足个输出即可。
最简单的方案
非常傻瓜的方式可以直接如下:
#define logs1(a) std::cout << OUT_GREEN << FILE_INFO << #a":" << a << OUT_END << std::endl
#define logs2(a,b) std::cout << OUT_GREEN << FILE_INFO << #a":" << a << ", "#b":" << b << OUT_END << std::endl
#define logs3(a,b,c) std::cout << OUT_GREEN << FILE_INFO << #a":" << a << ", "#b":" << b << ", "#c":" << c << OUT_END << std::endl
#define logs4...
#define logs5...
...
int one = 5;
int two = 3;
int three = 2;
logs1(one);
logs2(one, two);
logs3(one, two, three);
即多少个变量,对应用哪一个函数输出;由于宏不能够重载,所以不能用相同的名字!
但是这种方式有如下几个问题:
- 输出函数名称会很多。
- 还要数参数个数,与函数名对应。
- 如果想要不同颜色输出,则又要添加新的函数名。
计算参数个数
因为C/C++多参数输入,可以直接用 “…”代替,比如常见的 printf 函数就是如此!原型如下:
int __cdecl printf(const char * _Format,...);
只要我们能够动态计算出参数个数,就可以通过映射的方式,绑定到对应参数数目的输出函数上面。
那么如何计算呢?
经过苦苦查找,让我找到如下的方式:
#define ARG_COUNT_PRIVATE(_0, _1, _2, _3, _4, _5, _6, _7, _8, N, ...) N
#define ARG_COUNT(...) ARG_COUNT_PRIVATE(0, __VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#define FILE_INFO "[" << __FILE__ << '@' << __FUNCTION__ << '#' << __LINE__ << "]"
#define logs(a) std::cout << OUT_GREEN << FILE_INFO << #a":" << a << OUT_END << std::endl
...
int one = 5;
int two = 3;
int three = 2;
logs(ARG_COUNT(one));
logs(ARG_COUNT(one, two));
logs(ARG_COUNT(one, two, three));
输出结果:
[..\cppdemo\main.cpp@main#30]ARG_COUNT(one):1
[..\cppdemo\main.cpp@main#31]ARG_COUNT(one, two):2
[..\cppdemo\main.cpp@main#32]ARG_COUNT(one, two, three):3
然后只要我们在用C++宏里面的 “##”进行连接,就可以将 logs ## Num 变成对应的函数。
最后的源码
所有的问题都已经解决了,那么最后的代码就如下了。
#ifndef LOGGER_H
#define LOGGER_H
///================= package define =====================
#define ARG_COUNT_PRIVATE(\
_0, _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, _56, _57, _58, _59, \
_60, _61, _62, _63, _64, N, ...) N
#define ARG_COUNT(...) ARG_COUNT_PRIVATE(0, __VA_ARGS__,\
64, 63, 62, 61, 60, \
59, 58, 57, 56, 55, 54, 53, 52, 51, 50, \
49, 48, 47, 46, 45, 44, 43, 42, 41, 40, \
39, 38, 37, 36, 35, 34, 33, 32, 31, 30, \
29, 28, 27, 26, 25, 24, 23, 22, 21, 20, \
19, 18, 17, 16, 15, 14, 13, 12, 11, 10, \
9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#define FUN_COUNT_GLUE(M,count) M##count
#define FUN_JOIN_COUNT(M,count) FUN_COUNT_GLUE(M,count)
#define FUN_JOIN_ARGS(x, y) x y
#define CallSomeOne(fn, ...) FUN_JOIN_ARGS(FUN_JOIN_COUNT(fn, ARG_COUNT(__VA_ARGS__)), (__VA_ARGS__))
///================= logger =====================
/// 日志输出
///
#if defined QS_LOG
#include "QsLog.h"
#define PR QLOG_INFO() // QsLog 输出(一个用Qt封装的日志类,挺好用的,在此推荐一下)
#define ENDL ""
#elif defined QT_CORE_LIB // Qt 标准输出
#include <QDebug>
#define PR qDebug()
#define ENDL ""
#elif defined __cplusplus
#include <iostream>
using namespace std;
#define PR std::cout
#define ENDL std::endl
#endif
#define OUT_RED "\033[0;31;1m"
#define OUT_GREEN "\033[0;32;1m"
#define OUT_YELLOW "\033[0;33;1m"
#define OUT_BLUE "\033[0;34;1m"
#define OUT_END "\033[0m"
#define FILE_INFO "[" << __FILE__ << '@' << __FUNCTION__ << '#' << __LINE__ << "]"
#define param1(a) #a":" << a
#define param2(a,b) #a":" << a << ", "#b":" << b
#define param3(a,b,c) #a":" << a << ", "#b":" << b << ", "#c":" << c
#define param4(a,b,c,d) #a":" << a << ", "#b":" << b << ", "#c":" << c << ", "#d":" << d
#define pr0() "null param out"
#define pr1(...) param1(__VA_ARGS__)
#define pr2(...) param2(__VA_ARGS__)
#define pr3(...) param3(__VA_ARGS__)
#define pr4(...) param4(__VA_ARGS__)
#define pr5(a,b,c,d,e) pr3(a,b,c) << ", " << param2(d,e)
#define pr6(a,b,c,d,e,f) pr3(a,b,c) << ", " << param3(d,e,f)
#define pr7(a,b,c,d,e,f,g) pr4(a,b,c,d) << ", " << param3(e,f,g)
#define pr8(a,b,c,d,e,f,g,h) pr4(a,b,c,d) << ", " << param4(e,f,g,h)
#define pr9(a,b,c,d,e,f,g,h,i) pr8(a,b,c,d,e,f,g,h) << ", " << param1(i)
#define pr10(a,b,c,d,e,f,g,h,i,j) pr9(a,b,c,d,e,f,g,h,i) << ", " << param1(j)
//.... 有兴趣大家可以继续扩充
#define logStr(x) PR << FILE_INFO << x << ENDL // 原样输出,无需格式化
#define logDebug(...) PR << "" << "DEBUG " << FILE_INFO << CallSomeOne(pr, __VA_ARGS__) << ENDL
#define logInfo(...) PR << OUT_GREEN << "INFO " << FILE_INFO << CallSomeOne(pr, __VA_ARGS__) << OUT_END << ENDL
#define logWarn(...) PR << OUT_YELLOW << "WARN " << FILE_INFO << CallSomeOne(pr, __VA_ARGS__) << OUT_END << ENDL
#define logError(...) PR << OUT_RED << "ERROR " << FILE_INFO << CallSomeOne(pr, __VA_ARGS__) << OUT_END << ENDL
#endif // LOGGER_H
总结
由于受到工作方向的影响,有很多用法被局限在我们日常的工作方向中,不能很好的做出符合大家各自场景的东西。 然而我觉得学习,除了学到东西,更应该获取的是思维方式。 我的砖就抛到这里,希望对你们有用。 接下来的路就请各位小伙伴们自己走了!