8

自定义C/C++日志输出函数

 3 years ago
source link: https://kymjs.com/code/2020/08/07/01/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

自定义C/C++日志输出函数

cleardot.gif
2020-08-07 By 张涛 | 本文已被访问156次

版权声明:本文是开源实验室原创文章,如您转载必须以链接形式注明原文地址:https://kymjs.com/code/2020/08/07/01
在繁杂的项目中,日志打印必不可少。但是编写打印的工作,有时候是无趣的、繁琐的、浪费精力的。
如何能够快速、方便的编写打印;如何清晰、准确的定位;如何简单并优雅的实现;最后才能让我们摆脱这样枯燥的、重复的工作?
网上有很多强大的日志类工具,我也都使用过一些,有时候也并没有理想中的方便。今天我想分享给大家的一套我自己的解决方案。
对本文有任何问题,可加我的个人微信询问:kymjs123

在繁杂的项目中,日志打印必不可少。但是编写打印的工作,有时候是无趣的、繁琐的、浪费精力的。
如何能够快速、方便的编写打印;如何清晰、准确的定位;如何简单并优雅的实现;最后才能让我们摆脱这样枯燥的、重复的工作?
网上有很多强大的日志类工具,我也都使用过一些,有时候也并没有理想中的方便。今天我想分享给大家的一套我自己的解决方案。

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

由于受到工作方向的影响,有很多用法被局限在我们日常的工作方向中,不能很好的做出符合大家各自场景的东西。 然而我觉得学习,除了学到东西,更应该获取的是思维方式。 我的砖就抛到这里,希望对你们有用。 接下来的路就请各位小伙伴们自己走了!

了解更多有深度技术的文章,与移动端、大前端未来方向的认知,前往订阅 开源实验室小专栏。
张涛-qrcode


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK