【问题标题】:How to write a macro with optional and variadic arguments如何编写带有可选和可变参数的宏
【发布时间】:2017-08-06 09:15:24
【问题描述】:

我在我的代码中使用了一个名为 PRINT(...) 的宏,它获取可变数量的参数,其行为类似于 printf(获取格式和参数)。它是这样定义的:

#define PRINT(...) PRINT(__VA_ARGS__)                     

现在我想修改它,让它有一个可选参数,比如它的名字是number,它会在打印中添加一个数字前缀。例如:

PRINT("%s", "hi") -> 将打印 hi
PRINT(1, "%s", "hi") -> 将打印 1: hi

如何更改 PRINT 宏以支持此功能?
重要的是,我不想从我的代码更改对该宏的任何现有调用(在示例中,如果我调用了 PRINT("%s", "hi") - 更改后它需要保持不变)。
此外,我不能为此目的创建新宏 - 必须为此目的使用现有的 PRINT 宏(但当然我可以更改它的 Argumnts 定义)。
知道我该怎么做吗?

编辑:我看到了这个post 关于可变参数宏 - 但它与我在这里问的不同,因为参数number 需要是一个可识别的变量,它将在PRINT 的实现中被视为-1 如果对PRINT 的调用不包含number 参数(-1 将指示不打印数字),否则它将打印数字前缀。

【问题讨论】:

  • @TonyTannous ,编辑了我的帖子。您提供的链接没有解决我的问题
  • 考虑如何确定您的 number 参数是否存在。宏变体可能会更容易。
  • PRINT(...)的宏定义真的是PRINT(__VA_ARGS__)吗?
  • 现在请一次编辑帖子,以便它在一个地方包含您想要实现的所有必要要求

标签: c macros


【解决方案1】:

从 C11 开始,您可以使用 _Generic 关键字。这允许您检查任何值或变量的类型。根据this document_Generic 的行为因编译器而异。不过,This answer 提供了一个简单的解决方案,使用 comma operator

#define PRINT(fst, ...) \
( \
    _Generic((0, fst), char *: 1, default: 0) ? \
    PRINTNL(fst, __VA_ARGS__) : \
    PRINTL(fst, __VA_ARGS__) \
)

PRINTNL 打印不带数字,PRINTL 打印带数字。

其余代码:

#define PRINTNL(...) printf(__VA_ARGS__)
#define PRINTL (n, ...) ({ \
    printf("%d: ", n); \
    printf(__VA_ARGS__); \
})

【讨论】:

  • 字符串测试其实有点复杂,可能是char*char const*
  • 你是对的。它似乎也依赖于编译器。见this
  • 除了字符串测试和编译器依赖性之外,这对我来说将是一个非常有问题的解决方案,因为将来我可能想向 PRINT 宏添加更多可选参数 - 如果我这样做会使实现变得非常复杂使用这种方式(如果我理解正确的话)
  • 也可以是char数组!
  • @John 这绝对是使用新约束在 C 中实现这一目标的唯一方法。如果您不满意,则需要调整或更改语言。
【解决方案2】:

由于您在撰写本文时已经知道第一个参数是否是数字前缀,因此请使用另一个名称创建一个宏以作为数字前缀。这里我假设PRINT(...) 扩展为printf(__VA_ARGS__)

#define PRINT(...) printf(__VA_ARGS__)

所以定义一个宏NPRINT 调用printf 两次,一次输出带数字的前缀,一次输出格式:

#define NPRINT(number, fmt, ...) (printf("%d: ", number), printf(fmt, __VA_ARGS__))

用法

#include <stdio.h>

int main(void) {
    NPRINT(1, "%s\n", "hi");
}

当然,如果对 printf 的调用应该是 atomic - 现在如果格式字符串始终是 literal 字符串,那么你可以使用字符串连接:

#define NPRINT(number, fmt, ...) (printf("%d " fmt, number, __VA_ARGS__))

如果它可以是一个变量并且只允许一次调用PRINT,那么我能看到的唯一可移植方法是创建一个构建格式的函数。

在没有数字参数的最新编辑中,-1: 应该作为前缀,这将变成:

#define PRINT(...) NPRINT(-1, __VA_ARGS__)

【讨论】:

  • 这里的问题是我将不得不使用 NPRINT 来打印带有数字前缀,而我只能使用 PRINT 来达到这个目的
  • @John 唯一可能的解决方案是您是否可以使用 C11 和 _Generic 选择。那么标准C中绝对没有其他方法。
  • 这当然是正确的方法。重载(一种多态性形式;本质上是 OP 所要求的)是一种适应类型系统以使一个 conceptual 函数执行 one thing 的好方法,但是如果您要求一个概念性功能来做许多事情,那就大错特错了。 PRINT( [...arguments...] ) 是一回事。 PRINT( , , [...arguments...] ) 是另一回事。两件事不应该都拼写为PRINT;这是糟糕的设计。
【解决方案3】:

请查看##__VA_ARGS__ macro。还要检查下面为日志功能编写的代码。

在头文件中

/**
 * ##__VA_ARGS__ allows us to make varadic arguments optional
 * https://gcc.gnu.org/onlinedocs/gcc/Variadic-Macros.html
 * also check __VA_OPT__ (C++20)
 */ 

#define APP_LOG(message, ...) \
    do { \
            APP_LOGX( __FILE__, __LINE__, ("\e[1;34m[INFO]: \e[0m" message), ##__VA_ARGS__); /** BLUE */ \
    } \
    while(0)

#define APP_ERROR(message, ...) \
    do { \
            APP_LOGX( __FILE__, __LINE__, ("\e[1;31m[FATAL]: \e[0m" message), ##__VA_ARGS__); /** RED */ \
    } \
    while(0)

在实施中

/** #include <libgen.h> */
void APP_LOGX(const char * file, int num, const char* message, ...)
{
    va_list ap;
    int length;
    char * tfilename = NULL;
    tfilename = strdup(file);
    
    length = strlen(message);
    if(length>0){
        va_start(ap, message);
        vprintf(message, ap);
        printf(" | File: %s Line %d ", basename(tfilename), num);
        va_end(ap);

        /* add newline if nessasary */
        if(message[length -1] != '\n'){
            printf("\n");
        }
    }

    free(tfilename);
}

应用

 APP_LOG("app display init")

APP_LOG("Value of x acceleration %.2f", g);
APP_ERROR("Something bad happened!")

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-26
    • 2013-04-02
    • 2016-06-21
    • 2015-11-09
    • 1970-01-01
    相关资源
    最近更新 更多