如何在C语言宏(非函数)中访问并解包__VA_ARGS__内容
在函数中可以借助stdarg.h访问可变参数...的内容,示例如下:
void fn(int nargs, ...){ va_list args; va_start(args,nargs); i64 arg0 = va_arg(args,i64); va_end(args); }
目前常见的__VA_ARGS__用法多为将其传递给其他宏,或是最终传递给函数,例如:
#define __fn(...) fn(number_of_args(__VA_ARGS__), __VA_ARGS__)
想要直接在宏内部“解包”__VA_ARGS__的参数值,实现类似va_start()、va_arg()、va_end()的宏层面可变参数访问能力是完全可行的,只是标准C预处理器没有提供对应的原生接口,需要依托宏展开规则实现,以下是具体方案:
核心基础:按位置提取参数的宏
预处理器支持按位置匹配参数列表,我们可以先实现一套通用的取第N个参数的宏,这是所有可变参数解包逻辑的核心:
// 示例为最大支持8个参数的版本,需要支持更多参数按相同规律往后补定义即可 #define __GET_ARG_1(_1, ...) _1 #define __GET_ARG_2(_1, _2, ...) _2 #define __GET_ARG_3(_1, _2, _3, ...) _3 #define __GET_ARG_4(_1, _2, _3, _4, ...) _4 // ...... 可一直扩展到实际需要的最大参数数量 #define __GET_ARG(N, ...) __GET_ARG_##N(__VA_ARGS__)
比如调用__GET_ARG(2, a, b, c, d)时,宏会先拼接展开为__GET_ARG_2(a,b,c,d),最终返回第二个参数b,效果和va_arg按顺序取参的逻辑一致。
配套的参数计数实现
上述示例中用到的number_of_args(__VA_ARGS__)同样可以依托位置匹配逻辑实现,不需要额外依赖:
// 对应前面最大8个参数的配置 #define __ARGS_COUNT(...) __GET_ARG(9, __VA_ARGS__, 8,7,6,5,4,3,2,1,0)
实现逻辑非常直接:传入N个参数时,后面补的数字序列会整体向后偏移N位,第9个位置的数字正好就是参数个数N。比如传入3个参数x,y,z,展开后的参数列表为x,y,z,8,7,6,5,4,3,2,1,0,第9位正好是3,计数结果准确。
实现类va_list的宏内遍历能力
有了单参数提取和参数计数能力,就可以在宏内部逐个解包参数了,比如实现一个逐个处理所有可变参数的遍历逻辑:
// 辅助宏,解决宏递归展开的阻断问题 #define __MACRO_EVAL(...) __VA_ARGS__ #define __FOREACH_0(fn, ...) #define __FOREACH_1(fn, arg, ...) fn(arg) __MACRO_EVAL(__FOREACH_0(fn, __VA_ARGS__)) #define __FOREACH_2(fn, arg, ...) fn(arg) __MACRO_EVAL(__FOREACH_1(fn, __VA_ARGS__)) #define __FOREACH_3(fn, arg, ...) fn(arg) __MACRO_EVAL(__FOREACH_2(fn, __VA_ARGS__)) #define __FOREACH_4(fn, arg, ...) fn(arg) __MACRO_EVAL(__FOREACH_3(fn, __VA_ARGS__)) // ...... 同样按规律扩展到设定的最大参数数即可 #define __FOREACH(fn, ...) __MACRO_EVAL(__GET_ARG(__ARGS_COUNT(__VA_ARGS__)+1, __VA_ARGS__, __FOREACH_8,__FOREACH_7,__FOREACH_6,__FOREACH_5,__FOREACH_4,__FOREACH_3,__FOREACH_2,__FOREACH_1,__FOREACH_0)(fn, __VA_ARGS__)) // 使用示例:定义单个参数的处理逻辑 #define __PRINT_ONE(x) printf("arg value: %lld\n", (i64)x); // 直接在宏里遍历所有可变参数,不需要透传给函数 #define PRINT_ALL_ARGS(...) __FOREACH(__PRINT_ONE, __VA_ARGS__)
调用PRINT_ALL_ARGS(1,2,3)时,会直接在预处理阶段展开成三条独立的printf语句,完全在宏层面完成参数解包和遍历,不需要借助函数的va_list能力。
使用注意事项
- 这种实现方式存在固定的参数个数上限,需要根据使用场景提前扩展对应宏定义,一般工程场景下设到64或者128个参数足够覆盖绝大多数需求
- 预处理器不支持真正的递归宏,所以遍历类逻辑必须像示例中那样把每一层的展开手动写全,不能靠宏自指实现循环
- 如果使用支持C++20的编译环境,可以用标准新增的
__VA_OPT__简化可变参数为空时的边界判断逻辑,减少适配宏的代码量
内容的提问来源于stack exchange,提问作者étale-cohomology

