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



对本文有任何问题,可加我的个人微信询问:kymjs666

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

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

总结

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