如何将 printf() 包装到函数或宏中?

Posted

技术标签:

【中文标题】如何将 printf() 包装到函数或宏中?【英文标题】:How to wrap printf() into a function or macro? 【发布时间】:2014-01-05 12:55:31 【问题描述】:

这听起来像是一个面试问题,但实际上是一个实际问题。

我正在使用嵌入式平台,并且只有这些功能的等价物:

printf() snprintf()

此外,printf() 实现(和签名)可能会在不久的将来发生变化,因此对其的调用必须驻留在单独的模块中,以便以后轻松迁移。

鉴于此,我可以将日志调用包装在某个函数或宏中吗?目标是我的源代码在一千个地方调用THAT_MACRO("Number of bunnies: %d", numBunnies);,但对上述函数的调用只在一个地方看到。

编译器:arm-gcc -std=c99

编辑:只是提一下,但在 2000 年的最佳实践之后,可能更早,内联函数比宏要好得多,原因有很多。

【问题讨论】:

你的编译器支持可变参数宏吗? 有哪些编译器限制?如果这必须在 C99 之前的 C 版本上运行,则很难作为宏移植。 @KerrekSB 我想为什么?这几天 cmets 被屏蔽了? 【参考方案1】:

有两种方法可以做到这一点:

    变量宏

    #define my_printf(...) printf(__VA_ARGS__)
    

    转发va_args的函数

    #include <stdarg.h>
    #include <stdio.h>
    
    void my_printf(const char *fmt, ...) 
        va_list args;
        va_start(args, fmt);
        vprintf(fmt, args);
        va_end(args);
    
    

还有vsnprintfvfprintf 以及stdio 中你能想到的任何东西。

【讨论】:

此外,您可以找到一些关于宏(和可变参数宏)的文档here。 @Roddy 是的,如果您想转发所有参数。我不鼓励这样做,因为您不能以这种方式定义无操作宏。使用类似函数的宏,如果需要,您可以随时将其设为无操作。 我希望有人编辑了这个答案,这样我就可以删除我的赞成票。我没有 vprintf 或其他幻想。嵌入式,你知道的。 对不起语气。我真的不能为此使用标准库。这是一个基于 ARM 的定制平台。 我一直在尝试这样的各种方法,但是 1. 我无法制作通过命名空间工作的宏; #define Log::WriteLine(_Format, ...) printf(_Format, __VA_ARGS__) 不起作用。并且 2. 它没有以石灰绿色显示 %s 等,或者验证输入......因此作为替代品是无用的。如果有任何方法可以获得自定义 printf,以石灰绿色显示 %s 等,并进行必要的验证?【参考方案2】:

由于您可以使用 C99,我会将其包装在 variadic macro 中:

#define TM_PRINTF(f_, ...) printf((f_), __VA_ARGS__)
#define TM_SNPRINTF(s_, sz_, f_, ...) snprintf((s_), (sz_), (f_), __VA_ARGS__)

因为你没有说你有vprintf 或类似的东西。如果你确实有类似的东西,你可以将它包装在一个函数中,就像 Sergey L 在他的回答中提供的那样。


上述 TM_PRINTF 不适用于空的 VA_ARGS 列表。 至少在 GCC 中可以这样写:

#define TM_PRINTF(f_, ...) printf((f_), ##__VA_ARGS__)

如果__VA_ARGS__ 为空,则两个## 符号会删除它们前面多余的逗号。

【讨论】:

TM_PRINTF("Starting ei-oh!"); 产生error: expected expression before ')' token。如果没有格式字符串参数,它就可以工作。可变参数的数量是否需要非零? 看来上面的错误没有gcc扩展是无法修复的:gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html 您可以解决删除 _f 部分并仅保留 varidac 的问题。对我来说工作没有问题,但我不知道它是否超出标准 @lesto,是的,它“有效”,但删除 f_ 将使 TM_PRINTF() 允许。如果该字符串在代码中,编译器稍后会抱怨 printf() 与它的函数签名不匹配。在这种情况下,最好让编译器抱怨TM_PRINTF,因为这样更明显。 @ldav1s 我不明白你所说的“可变参数”宏是什么意思。那些省略号 ... 是如何神奇地将它们转发到 __VA_ARGS__ 的?【参考方案3】:

如果您可以忍受将调用包含在 两个 括号中,您可以这样做:

#define THAT_MACRO(pargs)    printf pargs

然后使用它:

THAT_MACRO(("This is a string: %s\n", "foo"));
           ^
           |
          OMG

这是可行的,因为从预处理器的角度来看,整个参数列表变成一个宏参数,用括号代替。

这比单纯的做要好

#define THAT_MACRO printf

因为它允许你定义它:

#define THAT_MACRO(pargs)  /* nothing */

这会“吃掉”宏参数,它们永远不会成为编译代码的一部分。

更新当然,在 C99 中这种技术已经过时了,只要使用可变参数宏就可以了。

【讨论】:

天啊,感谢您的出色回答。我想在接下来的几年里,你会得到所有可怜的 C89 程序员的支持 :) 这种技术的一个重要副作用:所有对THAT_MACRO 的调用都需要双括号,即使是单参数调用,例如THAT_MACRO(("Foo Bar"))。 --怀着爱,几年后一个可怜的C89程序员。【参考方案4】:
#define TM_PRINTF(f_, ...) printf((f_), ##__VA_ARGS__)

## 令牌将启用 TM_PRINTF("aaa");

【讨论】:

这就像魔术!每个案例都为我节省了很多重复的宏! ## 是如何工作的? 是的,这有效,而接受的答案却没有。谢谢! 请注意,这是一个编译器扩展,而不是 C99 标准。基本上,编译器编写者,他们是聪明人,认识到标准中的这种疏忽,并找到了解决方法。需要注意的是,它不能保证在每个符合 C99 的编译器上都能工作,并且一些编译器可能对同一件事使用不同的语法。【参考方案5】:
#define PRINTF(...) printf(__VA_ARGS__)

这样工作:

它定义参数化宏 PRINTF 以接受(最多)无限参数,然后将其从 PRINTF(...) 预处理到 printf(__VA_ARGS__)__VA_ARGS__ 在参数化宏定义中用于表示给定的参数(因为你不能命名无限参数,对吗?)。

【讨论】:

【参考方案6】:

图书馆有限?嵌入式系统?需要尽可能多的性能?没问题!

正如this question 的答案所示,您可以使用汇编语言将不接受 VA_LIST 的函数包装成可以接受的函数,以很少的成本实现您自己的 vprintf!

虽然这会起作用,并且几乎可以肯定会产生您想要的性能和抽象,但我还是建议您获得一个功能更丰富的标准库,也许可以通过对uClibc 的部分进行切片。除非您绝对需要每个周期,否则这样的解决方案肯定是比使用汇编更便携且整体更有用的答案。

毕竟,这就是存在此类项目的原因。

【讨论】:

@ΈρικΚωνσταντόπουλος 是的,这是极少数的系统之一,ASM 可以接受程序员时间的折衷。【参考方案7】:

这是@ldav1 出色答案的略微修改版本,它在日志之前打印时间:

#define TM_PRINTF(f_, ...)                                                                            \
                                                                                                 \
    struct tm _tm123_;                                                                            \
    struct timeval _xxtv123_;                                                                     \
    gettimeofday(&_xxtv123_, NULL);                                                               \
    localtime_r(&_xxtv123_.tv_sec, &_tm123_);                                                     \
    printf("%2d:%2d:%2d.%d\t", _tm123_.tm_hour, _tm123_.tm_min, _tm123_.tm_sec, _xxtv123_.tv_usec); \
    printf((f_), ##__VA_ARGS__);                                                                  \
;

【讨论】:

以上是关于如何将 printf() 包装到函数或宏中?的主要内容,如果未能解决你的问题,请参考以下文章

用于定义具有可变参数的函数的嵌套#define 问题

用 Clojure 包装

根据用户偏好过滤的包装器 printf 函数

实现一个登录函数或宏可以记录日期和函数名

Python 等价于内联函数或宏

确保每个字符串文字都包含在宏中