如何实现一个“万能”的调试打印函数

原文链接:http://www.juzicode.com/cpp-note-debug-print-code-model

在阅读第三方库或者大型工程的源码时,只是走读源码通常很难获得详细的代码运行过程,这时候我们可能就希望通过在源码中增加些输出信息再编译运行,从输出上就可以很方便地看到函数的调用关系,特别是包含很多if、for等流程控制语句时,通过插入打印信息可以观察到程序运行到哪个分支。

下面是一个简化处理的例子,通过在被调用函数内部的入口和出口分别输入2个cout语句,这样在main中就能方便的观察到程序的调用过程:

//juzicode.com/vx:桔子code
#include "iostream"
int add(int a, int b)
{
    std::cout << "add() enter-----" <<std::endl;
    int ret = a + b;
    /* ......
    */
    std::cout << "add() -----QUIT" << std::endl;
    return ret;
}
int sub(int a, int b) 
{
    std::cout << "sub() enter-----" << std::endl;
    int ret = a - b;
    /* ......
    */
    std::cout << "sub() -----QUIT" << std::endl;
    return ret;
}
int main(void)
{
    int sum = add(10, 60);
    int diff = sub(100, 50);
    return 0;
}

运行结果:

add() enter-----
add() -----QUIT
sub() enter-----
sub() -----QUIT

如果还希望观察到函数的入参的值是多少、流程控制语句的跳转情况,我们就需要再增加些语句:

int add(int a, int b)
{
    std::cout << "add() enter-----" <<std::endl;
    std::cout << "a=" << a << std::endl; //继续增加
    std::cout << "b=" << b << std::endl;
    int ret = a + b;
    /* ......
    */
    std::cout << "add() -----QUIT" << std::endl;
    return ret;
}

随着要观察的信息增多,需要输入越来越多的cout语句,对于桔子菌这种强迫症来说一个”<“符号需要同时按下shift和”<“键都觉得有点麻烦,有没有更简便的方法呢?

当然有了,就是用函数做封装

//juzicode.com/vx:桔子code
#include "iostream"
#include "string"
void dbgprt(std::string str)
{
    std::cout << str << std::endl;
}
void dbgprt(std::string str, int a)
{
    std::cout << str << " " << a << std::endl;
}
int add(int a, int b)
{
    dbgprt("add() enter-----");
    dbgprt("a=", a);
    dbgprt("b=", b);
    int ret = a + b;
    /* ......
    */
    dbgprt("add() -----quit");
    return ret;
}

运行结果:

add() enter-----
a= 10
b= 60
add() -----quit
sub() enter-----
sub() -----quit

看起来效果还不错,但是这里只定义了2种类型的dbgprt()函数,如果要是再有其他的float,double等类型入参的函数又得要重新增加一个入参是float或者double型的dbgprt()函数。

这里插播一句,虽然可以像下面这样直接调用dbgprt()函数不会出现编译错误:

double add(double a, double b)
{
    dbgprt("add() enter-----");
    dbgprt("a=", a);
    dbgprt("b=", b);
    double ret = a + b;
    /* ......
    */
    dbgprt("add() -----quit");
    return ret;
}
int main(void)
{
    double sum2 = add(10.1, 60.1);
    return 0;
}

但是运行结果显示,double型的入参a和b被转换成了整型的10和60了,而不是真实的10.1和60.1:

add() enter-----
a= 10
b= 60
add() -----quit

要解决这个问题,需要定义一个能将2个double型的变量显示出来的dbgprt()函数,但是随着dbgprt()入参个数的增加,假设要显示的入参种类有m种,就会出现n个入参有n*m种组合的情况,一个劲地重载n*m个dbgprt()函数头都要秃了。

其实我们可以使用函数模板的方法,下面就是将原来的2个dbgprt()函数改造后,不再需要指定函数的入参类型,交给编译器在编译的时候去自动推导参数类型选择合适的函数调用,这样处理后n个参数就只需要定义n个函数了。

template <typename T1>
void dbgprt(T1 v1)
{
    std::cout << v1 << std::endl;
}
template <typename T1, typename T2>
void dbgprt(T1 v1, T2 v2)
{
    std::cout << v1 << " "<< v2 << std::endl;
}

完整的例子:

//juzicode.com/vx:桔子code
#include "iostream"
#include "string"
template <typename T1>
void dbgprt(T1 v1)
{
    std::cout << v1 << std::endl;
}
template <typename T1, typename T2>
void dbgprt(T1 v1, T2 v2)
{
    std::cout << v1 << " "<< v2 << std::endl;
}
double add(double a, double b)
{
    dbgprt("add() enter-----");
    dbgprt("a=", a);
    dbgprt("b=", b);
    double ret = a + b;
    /* ......
    */
    dbgprt("add() -----quit");
    return ret;
}
int add(int a, int b)
{
    dbgprt("add() enter-----");
    dbgprt("a=", a);
    dbgprt("b=", b);
    int ret = a + b;
    /* ......
    */
    dbgprt("add() -----quit");
    return ret;
}
int sub(int a, int b) 
{
    dbgprt("sub() enter-----");
    dbgprt("a=", a);
    dbgprt("b=", b);
    int ret = a - b;
    /* ......
    */
    dbgprt("sub() -----quit");
    return ret;
}
int main(void)
{
    int sum = add(10, 60);
    double sum2 = add(10.1, 60.1);
    int diff = sub(100, 50);
    return 0;
} 

从下面的运行结果可以看到double类型的数据能正确显示了:

add() enter-----
a= 10
b= 60
add() -----quit
add() enter-----
a= 10.1     //这里可以看到double型的数值能正确显示了
b= 60.1
add() -----quit
sub() enter-----
a= 100
b= 50
sub() -----quit

前面的函数模板还只能适用最多2个参数的情况,如果希望能增加更多个数的入参,还可以像下面这样继续增加更多的函数模板

//juzicode.com/vx:桔子code
#include "iostream"
#include "string"
template <typename T1>
void dbgprt(T1 v1)
{
    std::cout << v1 << std::endl;
}
template <typename T1, typename T2>
void dbgprt(T1 v1, T2 v2)
{
    std::cout << v1 << " " << v2  <<std::endl;
}
template <typename T1, typename T2, typename T3>
void dbgprt(T1 v1, T2 v2, T3 v3)
{
    std::cout << v1 << " " << v2 << " " << v3 << std::endl;
}
template <typename T1, typename T2, typename T3, typename T4>
void dbgprt(T1 v1, T2 v2, T3 v3, T4 v4)
{
    std::cout << v1 << " " << v2 << " " << v3 << " " << v4 << std::endl;
}
template <typename T1, typename T2, typename T3, typename T4, typename T5>
void dbgprt(T1 v1, T2 v2, T3 v3, T4 v4, T5 v5)
{
    std::cout << v1 << " " << v2 << " " << v3 << " " << v4 << " " << v5 << std::endl;
}
......

到目前为止,解决了随着参数增加需要大量增加编写dbgprt()的问题。

当打印非常多的时候,满屏都是各种变量名称 ,为了分辨某一个变量是在哪个函数内部,往往还需要上下翻动查找在什么地方进入了哪个函数内部。比如前面的add()和sub()的例子中都有名称为a和b的变量,要分别清楚是在哪个函数内部的a和b的变量还需要往前往后翻看进入的是哪个函数,最好的解决办法是在输出变量名称前将函数名称也输出了,可以像下面这样在dbgprt()的第1个入参输入函数名称:

int add(int a, int b)
{
    dbgprt("add", "enter-----"); //在第1个入参加入函数名称
    dbgprt("add", " a=", a);
    dbgprt("add", " b=", b);
    int ret = a + b;
    /* ......
    */
    dbgprt("add", "-----quit");
    return ret;
}
int sub(int a, int b)
{
    dbgprt("sub", "enter-----");
    dbgprt("sub", " a=", a);
    dbgprt("sub", " b=", b);
    int ret = a - b;
    /* ......
    */
    dbgprt("sub", "-----quit");
    return ret;
}

运行结果:

add enter-----
add  a= 10
add  b= 60
add -----quit
sub enter-----
sub  a= 100
sub  b= 50
sub -----quit

这样看每个变量位于哪个函数内部就一目了然了。

但是,这样真的好吗,当有100个函数要跟踪的时候,你真的会一行一行的敲,而不是偷个懒复制粘贴再修改?修改的过程是不是会很烦人,会不会偶尔改错?更偷懒的办法就是不用每次输入不同的函数名称,而是用一个统一的“变量”?是的,有这么个宏:__FUNCTION__,它在哪个函数内部,就代表了哪个函数的字符串形式的名称:

int add(int a, int b)
{
    dbgprt( __FUNCTION__, "enter-----");
    dbgprt( __FUNCTION__, " a=", a);
    dbgprt( __FUNCTION__, " b=", b);
    int ret = a + b;
    /* ......
    */
    dbgprt( __FUNCTION__, "-----quit");
    return ret;
}
int sub(int a, int b) 
{
    dbgprt( __FUNCTION__, "enter-----");
    dbgprt( __FUNCTION__, " a=", a);
    dbgprt( __FUNCTION__, " b=", b);
    int ret = a - b;
    /* ......
    */
    dbgprt( __FUNCTION__, "-----quit");
    return ret;
}

运行结果:

add enter-----
add  a= 10
add  b= 60
add -----quit
sub enter-----
sub  a= 100
sub  b= 50
sub -----quit

其实从上面的代码可以看到,每个dbgprt()函数的第1个入参都是__FUNCTION__,何必要每次都输入呢,是不是想到把它直接挪到dbgprt()函数内部?试过之后是不行的,因为__FUNCTION__所代表的含义和它所在的位置强相关,它在哪个函数内部表示的就是哪个函数的名称,如果都挪到dbgprt()中最后显示的都是“dbgprt”了。

木有办法了吗?不是的,还是有办法的,召唤宏啊,因为宏是先在当前位置展开后再编译的,在哪个函数内部调用就在这个函数内部展开后再编译。可以像下面这样:

#define DBGPRT(...) dbgprt(__FUNCTION__,__VA_ARGS__)

int add(int a, int b)
{
    DBGPRT("enter-----");
    DBGPRT(" a=", a);
    DBGPRT(" b=", b);
    int ret = a + b;
    /* ......
    */
    DBGPRT("-----quit");
    return ret;
}
int sub(int a, int b) 
{
    DBGPRT("enter-----");
    DBGPRT(" a=", a);
    DBGPRT(" b=", b);
    int ret = a - b;
    /* ......
    */
    DBGPRT("-----quit");
    return ret;
}

上面这样就和在函数内部调用dbgprt( __FUNCTION__, “enter—–“)效果一致了。

有时候我们可能在发布程序的时候不想将过多的内容显示出来,这时就需要再增加一个开关PRT_LEVEL,在dbgprt()函数内部根据这个开关做判断,决定是否输出打印:


#define PRT_LEVEL 1

template <typename T1>
void dbgprt(int level,T1 v1)
{
    if (level < PRT_LEVEL) return;
    std::cout << v1 << std::endl;
}
template <typename T1, typename T2>
void dbgprt(int level, T1 v1, T2 v2)
{
    if (level < PRT_LEVEL) return;
    std::cout << v1 << " " << v2 << std::endl;
}
template <typename T1, typename T2, typename T3>
void dbgprt(int level, T1 v1, T2 v2, T3 v3)
{
    if (level < PRT_LEVEL) return;
    std::cout << v1 << " " << v2 << " " << v3 << std::endl;
}

#define DBGPRT(...) dbgprt(1,__FUNCTION__,__VA_ARGS__)

像上面这样调用DBGPRT(…),在发布程序的时候不需要显示过多的打印内容,增大PRT_LEVEL的值就可以关闭输出了。

有了这些内容,再将dbgprt()这些内容封装到头文件中,在其他文件内包含这个头文件就可以愉快地用DBGPRT(…)跟踪代码了。

完整的头文件代码:

//juzicode.com/vx:桔子code

#ifndef __DBGPRT
#define __DBGPRT
#include <iostream>
#include <string>

#define PRT_LEVEL 1

template <typename T1>
void dbgprt(int level,T1 v1)
{
    if (level < PRT_LEVEL) return;
    std::cout << v1 << std::endl;
}
template <typename T1, typename T2>
void dbgprt(int level, T1 v1, T2 v2)
{
    if (level < PRT_LEVEL) return;
    std::cout << v1 << " " << v2 << std::endl;
}
template <typename T1, typename T2, typename T3>
void dbgprt(int level, T1 v1, T2 v2, T3 v3)
{
    if (level < PRT_LEVEL) return;
    std::cout << v1 << " " << v2 << " " << v3 << std::endl;
}

template <typename T1, typename T2, typename T3, typename T4>
void dbgprt(int level, T1 v1, T2 v2, T3 v3, T4 v4)
{
    if (level < PRT_LEVEL) return;
    std::cout << v1 << " " << v2 << " " << v3 << " " << v4 << std::endl;
}
template <typename T1, typename T2, typename T3, typename T4, typename T5>
void dbgprt(int level, T1 v1, T2 v2, T3 v3, T4 v4, T5 v5)
{
    if (level < PRT_LEVEL) return;
    std::cout << v1 << " " << v2 << " " << v3 << " " << v4 << " " << v5 << std::endl;
}
template <typename T1, typename T2, typename T3, typename T4, typename T5, typename T6>
void dbgprt(int level, T1 v1, T2 v2, T3 v3, T4 v4, T5 v5, T6 v6)
{
    if (level < PRT_LEVEL) return;
    std::cout << v1 << " " << v2 << " " << v3 << " " << v4 << " " << v5 << " " << v6 << std::endl;
}
template <typename T1, typename T2, typename T3, typename T4, typename T5, typename T6, typename T7>
void dbgprt(int level, T1 v1, T2 v2, T3 v3, T4 v4, T5 v5, T6 v6, T7 v7)
{
    if (level < PRT_LEVEL) return;
    std::cout << v1 << " " << v2 << " " << v3 << " " << v4 << " " << v5 << " " << v6 << " " << v7 << std::endl;
}

#define DBGPRT(...) dbgprt(1,__FUNCTION__,__VA_ARGS__)

#endif

推荐阅读:

论如何把自己变成卡通人物

有了这款神器,什么吃灰文件都统统现形

一行代码深度定制你的专属二维码(amzqr)

好冷的Python~条件语句的短路原则

桔子菌和超市老板田大爷的一次角色互换经历

pyautogui: 有了Python键盘鼠标都可以省着用了

改造getpass,强迫症患者再也不用担心少输字符了

Python logging正在忠实记录你的一举一动

来看看怎么用OpenCV解构Twitter大牛的视觉错觉图

老板,帮忙把这份Python代码打个包

posted @ 2021-07-30 23:56  桔子code  阅读(105)  评论(0编辑  收藏  举报