用可变参数包装一个宏包装器的C++代码?

4

问题

我有一种方法可以通过将函数替换为宏来包装函数,以便记录调用和返回代码。以下是一个可行的示例:

int rc;
int foo(int a, int b);
int bar(int a, char *b, int *c);

void    LogRet(char *fn, char *file, char *from, int ln, int ret)
{
    printf("%s.%s.%d: %s()  ret:%08x\n", file, from, ln, fn, ret);
}   

#define foo(args, ...)  (rc = (foo)(args, ##__VA_ARGS__), LogRet("foo", __FILE__, __FUNCTION__, __LINE__, rc), rc)

#define bar(args, ...)  (rc = (bar)(args, ##__VA_ARGS__), LogRet("bar", __FILE__, __FUNCTION__, __LINE__, rc), rc)

宏替换函数的宏调用该函数,记录函数名称及其被调用的位置以及返回代码。在语法上,任何函数都可以使用同样的方式进行包装,只需要在宏中替换函数名即可。我想做的是创建一个包装宏,其中foo的重新定义宏将类似于:

#define foo(args, ...) WRAPPPER(foo)

我了解stringify和double stringify的基本概念,但是我甚至无法让WRAPPER宏执行真正的函数调用。理想情况下,我希望将其简化为单个WRAP(foo)语句。原因是,我有大约100个或更多需要包装的函数,并且希望通过一个简单的强制包含文件来实现简单的包装。在放弃这个想法之前,我得出结论这是不可能的,但我想在这里询问一下。我正在使用clang和vc++,但最好能够在任何编译器上使用,因为我要调试很多不同的系统。

Jonathan Leffler答案的适应性

由于我是新手,所以我不确定这是否应该是单独的答案还是更新编辑。这基本上是Jonathan Leffler的答案。这是一个功能示例。虽然它调用的两个函数毫无意义,但目标是拥有一个可以用任何参数列表包装任何函数的宏。我主要用于记录大型代码库中的使用流程,该库存在问题。此外,我的原始示例有一个明显的缺陷。通过在全局变量中存储返回代码,它不是线程安全的,除非通过繁琐的TLS准备。现在已经删除全局变量,宏不再使用序列运算符返回值,它被保留并由日志函数返回。此外,正如Augurar指出的,并在Jonathan的示例中显示的那样,如果在同一文件中声明宏和函数声明,则需要括号。

#include <stdio.h>
#include <string.h>

int foo(int a, int b);
int bar(int a, char *b, int *c);


#if defined (_DEBUG)  || defined (DEBUG)
// Short version of __FILE__ without path requires runtime parsing
#define __SFILE__ (strrchr(__FILE__, '\\') ? strrchr(__FILE__, '\\') + 1 : __FILE__)
#ifdef WIN32
  #define WRAPPER(func, ...) LogRet(#func, __SFILE__, __FUNCTION__, __LINE__, (func)(__VA_ARGS__))
#else
  #define WRAPPER(func, ...) LogRet(#func, __SFILE__, __func__, __LINE__, (func)(__VA_ARGS__))
#endif

inline  int LogRet(const char *fn, const char *file, const char *from, int ln, int ret)
{
    printf("%s.%s.%d: %s()  ret:%08x\n", file, from, ln, fn, ret);
    return ret;
}   

#define foo(...) WRAPPER(foo, __VA_ARGS__)
#define bar(...) WRAPPER(bar, __VA_ARGS__)

#endif


int main(void)
{
    int x = foo(1, 2);
    bar(2, "doubled", &x);
    
    return 0;
}   

#ifdef foo
  #undef foo
  #undef bar
#endif

// If and only if the function definition is in the same file with the macros, you must either undefine the macros or
// parenthesize the function  e.g.   int (foo)(int a, int b) { ... }

int foo(int a, int b)   
{
    printf("%d + %d = %d\n", a, b, a + b);
    return a + b;
}   

int (bar)(int a, char *b, int *c)
{
    printf("%d  %s = %d\n", *c, b, a * *c);
    return *c * a;
}

发布构建输出:

1 + 2 = 3
3  doubled = 6

调试构建输出:

1 + 2 = 3
test.cpp.main.35: foo()  ret:00000003
3  doubled = 6
test.cpp.main.36: bar()  ret:00000006

主要的好处是不需要在代码中查找每个foo()或bar()的出现,以插入调试打印来记录调用和结果,或者您想要插入的任何调试代码。

你的意图有点不太清楚。在第一个代码块中,foo 宏似乎是递归定义的。此外,包括你尝试定义 WRAPPER 的代码(即使不成功)会更有帮助。 - augurar
foo宏替换了foo()函数调用,使用“debug”版本调用实际的函数。这就是为什么在宏中使用foo()作为(foo)(),这将防止其扩展为宏并调用实际函数。然后,该宏打印一个带有函数名称和返回代码的调试消息,然后确保真正的代码获得返回代码。之所以在宏中执行此操作,是因为这意味着根本不需要更改代码即可获得记录的调试运行。这些宏可以轻松地包含在调试构建中或不包含。示例宏确实有效。 - ChocoBilly
啊,我明白了。我想在声明函数名时也必须加括号。有点别扭,但我猜这样可以行得通。 - augurar
不需要添加函数声明的括号或进行任何不同的编码。如果定义了宏,预处理器就会知道“foo”和“(foo)”之间的区别。常见用法是在无法轻松覆盖库中的malloc()时捕获它。您将宏malloc定义为您的函数,然后当您的代码完成所需的调试跟踪后,使用(malloc)(size) 调用malloc;这适用于我所使用过的每个编译器。括号明确告诉编译器不要使用宏。这些宏通常在#if DEBUG或#if MYDEBUG中,并且美妙之处在于不需要更改代码。 - ChocoBilly
再次感谢Jonathan提供的格式化技巧。我还不得不在LogRet()参数中添加“const”,这是你建议的。VC++没有问题,但Clang不行。Clang也不喜欢__func__(这次格式没错)。我认为我可以将rc声明为thread_local,但这样不太可移植,而且没有存储会更整洁。 - ChocoBilly
显示剩余3条评论
1个回答

4

这段代码似乎符合您的要求:

#include <stdio.h>

int rc;
int foo(int a, int b);
int bar(int a, char *b, int *c);

extern void LogRet(const char *fn, const char *file, const char *from, int ln, int ret);

void LogRet(const char *fn, const char *file, const char *from, int ln, int ret)
{
    printf("%s.%s.%d: %s()  ret:%08x\n", file, from, ln, fn, ret);
}

#define foo(args, ...)  (rc = (foo)(args, ##__VA_ARGS__), LogRet("foo", __FILE__, __FUNCTION__, __LINE__, rc), rc)

#define bar(args, ...)  (rc = (bar)(args, ##__VA_ARGS__), LogRet("bar", __FILE__, __FUNCTION__, __LINE__, rc), rc)

extern void caller1(void);

void caller1(void)
{
    int d;
    int e = foo(1, 2);
    int f = bar(3, "abc", &d);
    printf("%s(): %d + %d + %d = %d\n", __func__, d, e, f, d + e + f);
}

#undef foo
#undef bar

#define WRAPPER(func, ...) ((rc = (func)(__VA_ARGS__)), LogRet(#func, __FILE__, __func__, __LINE__, rc), rc)

#define foo(...) WRAPPER(foo, __VA_ARGS__)
#define bar(...) WRAPPER(bar, __VA_ARGS__)

extern void caller2(void);

void caller2(void)
{
    int d;
    int e = foo(2, 3);
    int f = bar(3, "abc", &d);
    printf("%s(): %d + %d + %d = %d\n", __func__, d, e, f, d + e + f);
}

int (foo)(int a, int b)
{
    return (a + b) % 3;
}

int (bar)(int a, char *b, int *c)
{
    int d = b[a];
    *c = d;
    return a + d;
}

int main(void)
{
    caller1();
    caller2();
    return 0;
}

示例输出:

wrapper.c.caller1.23: foo()  ret:00000000
wrapper.c.caller1.24: bar()  ret:00000003
caller1(): 0 + 0 + 3 = 3
wrapper.c.caller2.41: foo()  ret:00000002
wrapper.c.caller2.42: bar()  ret:00000003
caller2(): 0 + 2 + 3 = 5

预处理后的源代码(不包括来自#include <stdio.h>的输出):

# 2 "wrapper.c" 2

int rc;
int foo(int a, int b);
int bar(int a, char *b, int *c);

extern void LogRet(const char *fn, const char *file, const char *from, int ln, int ret);

void LogRet(const char *fn, const char *file, const char *from, int ln, int ret)
{
    printf("%s.%s.%d: %s()  ret:%08x\n", file, from, ln, fn, ret);
}





extern void caller1(void);

void caller1(void)
{
    int d;
    int e = (rc = (foo)(1, 2), LogRet("foo", "wrapper.c", __FUNCTION__, 23, rc), rc);
    int f = (rc = (bar)(3, "abc", &d), LogRet("bar", "wrapper.c", __FUNCTION__, 24, rc), rc);
    printf("%s(): %d + %d + %d = %d\n", __func__, d, e, f, d + e + f);
}
# 36 "wrapper.c"
extern void caller2(void);

void caller2(void)
{
    int d;
    int e = ((rc = (foo)(2, 3)), LogRet("foo", "wrapper.c", __func__, 41, rc), rc);
    int f = ((rc = (bar)(3, "abc", &d)), LogRet("bar", "wrapper.c", __func__, 42, rc), rc);
    printf("%s(): %d + %d + %d = %d\n", __func__, d, e, f, d + e + f);
}

int (foo)(int a, int b)
{
    return (a + b) % 3;
}

int (bar)(int a, char *b, int *c)
{
    int d = b[a];
    *c = d;
    return a + d;
}

int main(void)
{
    caller1();
    caller2();
    return 0;
}

在Mac OS X 10.9.2上使用GCC 4.8.2进行测试。


没错,谢谢!我在传递变量参数给包装器时搞混了。唯一需要的是一个#ifdef WIN32,因为MS VC很晚才加入这个程序,并且不识别__func__,只识别__FUNCTION__。在VS 2008、VS2010和VS 2012上都不行。我还没有尝试过VS 2013。我还没有研究__func__,因为我不能在所有地方使用它,也不确定它的好处。我知道__FILE__像一个C字符串。我使用自己的宏__SFILE__来剥离__FILE__的路径并仅打印文件名,但不想在示例中弄乱它。再次感谢,这将得到更多的重用。 - ChocoBilly
对于那个评论的格式问题我表示歉意。我不知道双下划线会被如何解释,然后编辑超时了。 - ChocoBilly
1
注释格式化很有趣,下划线和星号都可以触发_斜体_、粗体粗斜体。使用反引号来保护大多数包含__的单词。__func____FUNCTION__之间的选择完全取决于您;__func__是C99,但GCC也支持__FUNCTION__。其他编译器可能对正确性有不同的看法。C标准说__func__,但你的情况可能有所不同。 - Jonathan Leffler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接