宏可以通过参数数量进行重载吗?

45

这个 this 是如何工作的?如何实现一个 C99/C++11 可变参数宏,使其根据给定的参数数量而展开为不同的内容?


2
不解释其工作原理,但如果有人只需要实现:boost preprocessor - dyp
5个回答

65

(编辑:查看末尾以获取现成的解决方案。)

要获得一个重载的宏,首先需要一个可以在多个实现之间进行选择的宏。这部分不使用可变参数宏。然后,一个可变参数的宏通常计算其参数并生成一个选择器。将参数计数插入调度程序中即可生成重载宏。

注意:该系统无法区分零个和一个参数,因为没有区别于无参数和单个空参数。它们都看起来像MACRO()


要在实现之间进行选择,使用宏拼接运算符与一系列类似函数的宏一起使用。

#define select( selector, ... ) impl ## _ ## selector( __VA_ARGS__ )
#define impl_1() meh
#define impl_2( abc, xyz ) # abc "wizza" xyz()
//etc

// usage: select( 1 ) => impl_1() => meh
//        select( 2, huz, bar ) => impl_2( huzza, bar ) => "huz" "wizza" bar()

因为##操作符会抑制宏的展开,所以更好的做法是将其包装在另一个宏中。

#define CAT( A, B ) A ## B
#define SELECT( NAME, NUM ) CAT( NAME ## _, NUM )

要计算参数数量,使用__VA_ARGS__将参数移位,像这样(这是聪明的部分):

#define GET_COUNT( _1, _2, _3, _4, _5, _6 /* ad nauseam */, COUNT, ... ) COUNT
#define VA_SIZE( ... ) GET_COUNT( __VA_ARGS__, 6, 5, 4, 3, 2, 1 )

库代码:

#define CAT( A, B ) A ## B
#define SELECT( NAME, NUM ) CAT( NAME ## _, NUM )

#define GET_COUNT( _1, _2, _3, _4, _5, _6 /* ad nauseam */, COUNT, ... ) COUNT
#define VA_SIZE( ... ) GET_COUNT( __VA_ARGS__, 6, 5, 4, 3, 2, 1 )

#define VA_SELECT( NAME, ... ) SELECT( NAME, VA_SIZE(__VA_ARGS__) )(__VA_ARGS__)

用法:

#define MY_OVERLOADED( ... ) VA_SELECT( MY_OVERLOADED, __VA_ARGS__ )
#define MY_OVERLOADED_1( X ) foo< X >
#define MY_OVERLOADED_2( X, Y ) bar< X >( Y )
#define MY_OVERLOADED_3( X, Y, Z ) bang_ ## X< Y >.Z()

谢谢,这是一个很好的解释。也许在“clever”部分再提供一些细节会更好。 - Oli
3
假设您向 VA_SIZE 传递一个参数。它将扩展为 GET_COUNT(x, 6, 5, 4, 3, 2, 1),这会取消 x_16_2等等,最终留下 1 成为 COUNT。如果传递两个参数,就会再推入一个参数,2 变成了 COUNT - chris
@chris,确实,那很聪明。 - Oli
@Oli,没错,但很遗憾它只能处理有限数量的参数,因此才会出现“无休止”的情况。你可以随时添加尽可能多的支持。 - chris
1
请注意,在MSVC中似乎存在一个错误。这里有一个解决方法:https://dev59.com/Pm435IYBdhLWcg3w7ksK - Asaf
显示剩余3条评论

5
以下是对Potatoswatter's answer的改进,它可以区分零个参数和一个参数。
简而言之,当__VA_ARGS__为空时,在VA_SIZE宏中的EXPAND __VA_ARGS__()变成EXPAND ()并被替换为6个逗号。所以,VA_SIZE...变成了COMPOSE( GET_COUNT, (,,,,,, , 0, 6, 5, 4, 3, 2, 1) ),然后变成了GET_COUNT (,,,,,, , 0, 6, 5, 4, 3, 2, 1)并返回0。
另一方面,当__VA_ARGS__int, 5时,EXPAND __VA_ARGS__()变成了EXPAND int, 5()。因此,VA_SIZE...变成了COMPOSE(GET_COUNT,(EXPAND int, 5(),0,6,5,4,3,2,1)),它变成了GET_COUNT(EXPAND int, 5(),0,6,5,4,3,2,1)并返回2,如Potatoswatter的答案所述。
我从Jason Dang的答案中得到了EXPAND的想法。

库代码:

#define CAT( A, B ) A ## B
#define SELECT( NAME, NUM ) CAT( NAME ## _, NUM )
#define COMPOSE( NAME, ARGS ) NAME ARGS

#define GET_COUNT( _0, _1, _2, _3, _4, _5, _6 /* ad nauseam */, COUNT, ... ) COUNT
#define EXPAND() ,,,,,, // 6 commas (or 7 empty tokens)
#define VA_SIZE( ... ) COMPOSE( GET_COUNT, (EXPAND __VA_ARGS__ (), 0, 6, 5, 4, 3, 2, 1) )

#define VA_SELECT( NAME, ... ) SELECT( NAME, VA_SIZE(__VA_ARGS__) )(__VA_ARGS__)

使用方法:

#define MY_OVERLOADED( ... ) VA_SELECT( MY_OVERLOADED, __VA_ARGS__ )

#define MY_OVERLOADED_0( ) meh()
#define MY_OVERLOADED_1( X ) foo< X >
#define MY_OVERLOADED_2( X, Y ) bar< X >( Y )
#define MY_OVERLOADED_3( X, Y, Z ) bang_ ## X< Y >.Z()

MY_OVERLOADED()                // meh()
MY_OVERLOADED(bool)            // foo< bool >
MY_OVERLOADED(int, 5)          // bar< int >( 5 )
MY_OVERLOADED(me, double, now) // bang_me< double >.now()

1
@CaptJak 不是我的错。这个例子是由 OP 提出的。 :D - Super-intelligent Shade
这给了我一个错误信息:ISO C99 要求使用剩余参数。而且由于项目原因,我必须使用 gcc 的 -pedantic 开关。 - Th. Thielemann

5

我会把这个作为对Potatoswatter帖子的评论,但是它太长了,需要一个代码清单。

下面是一段用于生成一组可重载宏的Perl代码。

$ perl -le 'map{
        $arity = $_; map {
                $ar = 2 + $arity + $_; $arm = $ar - 1; $arlist = join("", map{"A$_, "} 1..$arity); $warlist = "WHAT, $arlist";
                @li = map {"_$_"} 0..$_; $lis = join(", ", @li); $lim = pop @li; $lims = join(", ", @li);
                print "#define FEI_${arity}A_$ar($warlist$lis) FEI_${arity}A_$arm($warlist$lims) WHAT($_, $arlist$lim)"
        } 1..3; print ""
} 0..4'

这是脚本的输出结果:
#define FEI_0A_3(WHAT, _0, _1) FEI_0A_2(WHAT, _0) WHAT(1, _1)
#define FEI_0A_4(WHAT, _0, _1, _2) FEI_0A_3(WHAT, _0, _1) WHAT(2, _2)
#define FEI_0A_5(WHAT, _0, _1, _2, _3) FEI_0A_4(WHAT, _0, _1, _2) WHAT(3, _3)

#define FEI_1A_4(WHAT, A1, _0, _1) FEI_1A_3(WHAT, A1, _0) WHAT(1, A1, _1)
#define FEI_1A_5(WHAT, A1, _0, _1, _2) FEI_1A_4(WHAT, A1, _0, _1) WHAT(2, A1, _2)
#define FEI_1A_6(WHAT, A1, _0, _1, _2, _3) FEI_1A_5(WHAT, A1, _0, _1, _2) WHAT(3, A1, _3)

#define FEI_2A_5(WHAT, A1, A2, _0, _1) FEI_2A_4(WHAT, A1, A2, _0) WHAT(1, A1, A2, _1)
#define FEI_2A_6(WHAT, A1, A2, _0, _1, _2) FEI_2A_5(WHAT, A1, A2, _0, _1) WHAT(2, A1, A2, _2)
#define FEI_2A_7(WHAT, A1, A2, _0, _1, _2, _3) FEI_2A_6(WHAT, A1, A2, _0, _1, _2) WHAT(3, A1, A2, _3)

#define FEI_3A_6(WHAT, A1, A2, A3, _0, _1) FEI_3A_5(WHAT, A1, A2, A3, _0) WHAT(1, A1, A2, A3, _1)
#define FEI_3A_7(WHAT, A1, A2, A3, _0, _1, _2) FEI_3A_6(WHAT, A1, A2, A3, _0, _1) WHAT(2, A1, A2, A3, _2)
#define FEI_3A_8(WHAT, A1, A2, A3, _0, _1, _2, _3) FEI_3A_7(WHAT, A1, A2, A3, _0, _1, _2) WHAT(3, A1, A2, A3, _3)

#define FEI_4A_7(WHAT, A1, A2, A3, A4, _0, _1) FEI_4A_6(WHAT, A1, A2, A3, A4, _0) WHAT(1, A1, A2, A3, A4, _1)
#define FEI_4A_8(WHAT, A1, A2, A3, A4, _0, _1, _2) FEI_4A_7(WHAT, A1, A2, A3, A4, _0, _1) WHAT(2, A1, A2, A3, A4, _2)
#define FEI_4A_9(WHAT, A1, A2, A3, A4, _0, _1, _2, _3) FEI_4A_8(WHAT, A1, A2, A3, A4, _0, _1, _2) WHAT(3, A1, A2, A3, A4, _3)

这些是用于生成可以将WHAT宏分发到一个参数列表中的任意数量的常量参数(A1A2 ...)以及按正确顺序具有索引的任意数量的参数的FOR_EACH(又名FE)宏的组的(定期结构化的)过载。 (不使用像SELECT一样的重载机制的幼稚实现会产生反向索引)。例如,第二个块的非规则“基本情况”部分如下所示:
#define FE_INDEXED_1ARG(...) VA_SELECT(FEI_1A, __VA_ARGS__)
#define FEI_1A_3(WHAT, A1, _0) WHAT(0, A1, _0)

这可能会被怀疑其实用性(我建立它是因为我看到了它的用处……),而且也没有直接回答OP的问题(事实上,它有点相反——一个foreach结构对所有可变参数做同样的事情……),但我只是认为这种技术非常有趣(某些方面也非常可怕),并且使用预处理器可以提供相当多的表达能力,并且可以以此方式生成非常高效的机器代码。我认为这也是一个令人深思的例子,说明了我个人认为C预处理器仍有改进余地的原因。

我的意思是,C预处理器是一个绝对的灾难,我们可能应该放弃它并重头开始:)


4

虽然已经有人回答了,但我准备了一个非常简短的版本,希望能对你有所帮助。

实施

// Variable Argument Macro (VA_MACRO) upto 6 arguments
#define NUM_ARGS_(_1, _2, _3, _4, _5, _6, TOTAL, ...) TOTAL
#define NUM_ARGS(...) NUM_ARGS_(__VA_ARGS__, 6, 5, 4, 3, 2, 1)

#define CONCATE_(X, Y) X##Y  // Fixed the double '_' from previous code
#define CONCATE(MACRO, NUMBER) CONCATE_(MACRO, NUMBER)
#define VA_MACRO(MACRO, ...) CONCATE(MACRO, NUM_ARGS(__VA_ARGS__))(__VA_ARGS__)

自定义

// This is how user may define own set of variadic macros
#define MY_MACRO(...) VA_MACRO(MY_MACRO, __VA_ARGS__)
#define MY_MACRO1(_1) "One"
#define MY_MACRO2(_1, _2) "Two"
#define MY_MACRO3(_1, _2, _3) "Three"

使用方法

// While using those, user needs to use only the main macro
int main ()
{
  auto one = MY_MACRO(1);
  auto two = MY_MACRO(1, 2); 
  auto three = MY_MACRO(1, 2, 3); 
}

这与我的答案有何不同之处?我唯一看到的是双下划线,这是一些编译器会标记的真正错误。 - Potatoswatter
1
@Potatoswatter,这并没有太大的区别,只是更易于理解、通用化、简化,并且宏的命名及其参数更加清晰易懂,适合新手。当我看到你的回答时,我完全迷失了方向,但在参考了你的回答后,我自己找到了“实际”的解决方案。关于__,我故意保留了它(正如您在评论中所看到的),因为通常人们会有自己的CONCATECONCATE_组合来连接两个字符串,因此那部分将自然而然地消失。把'__'看作是一个占位符以便理解。 - iammilind
它并不是通用的。实际上,这段代码几乎完全相同,但没有任何解释附加。也许我应该先放代码,再放描述。__ 是一个错误;它被保留给编译器实现者,不允许用户使用。 - Potatoswatter
2
发现这个答案很有帮助,因为代码更易于阅读。阅读这个答案后,更容易理解@Potatoswatter的回答。所以也给这个答案点了赞。 - Otzen

0
我扩展了Potatowatter的解决方案,以避免在使用gcc编译器开关-pedantic时出现iso c99 requires rest arguments to be used问题。
也可以使用零个参数。

#define NUM_ARGS_(_1, _2, _3, _4, _5, _6, _7, _8, TOTAL, ...) TOTAL
#define NUM_ARGS(...) NUM_ARGS_(__VA_ARGS__, 6, 5, 4, 3, 2, 1, 0)
#define CONCATE_(X, Y) X##Y
#define CONCATE(MACRO, NUMBER) CONCATE_(MACRO, NUMBER)
#define VA_MACRO(MACRO, ...) CONCATE(MACRO, NUM_ARGS (__VA_ARGS__))(__VA_ARGS__)

定制

注意: 在此示例中,MacroTest是你的重载C++函数。但你可以在这里放置任何函数。

#define MY_OVERLOADED(...) VA_MACRO(MY_OVERLOADED, void, void, __VA_ARGS__)
#define MY_OVERLOADED0(s, t) MacroTest()
#define MY_OVERLOADED1(s, t, a) MacroTest( a)
#define MY_OVERLOADED2(s, t, a, b) MacroTest(a, b)
#define MY_OVERLOADED3(s, t, a, b, c) MacroTest(a, b, c)

使用方法

MY_OVERLOADED();
MY_OVERLOADED(1);
MY_OVERLOADED(11, 22);
MY_OVERLOADED(111, 222, 333);

完整示例

已在https://www.onlinegdb.com/online_c++_compilerhttps://www.jdoodle.com/online-compiler-c++/上进行测试。

#include <stdio.h>

#define NUM_ARGS_(_1, _2, _3, _4, _5, _6, _7, _8, TOTAL, ...) TOTAL
#define NUM_ARGS(...) NUM_ARGS_(__VA_ARGS__, 6, 5, 4, 3, 2, 1, 0)
#define CONCATE_(X, Y) X##Y
#define CONCATE(MACRO, NUMBER) CONCATE_(MACRO, NUMBER)
#define VA_MACRO(MACRO, ...) CONCATE(MACRO, NUM_ARGS (__VA_ARGS__))(__VA_ARGS__)

void MacroTest() { printf("no param\n"); }
void MacroTest(int p1) { printf("p1=%i\n", p1); }
void MacroTest(int p1, int p2) { printf("p1=%i p2=%i\n", p1, p2); }
void MacroTest(int p1, int p2, int p3) { printf("p1=%i p2=%i p3=%i\n", p1, p2, p3); }

#define MY_OVERLOADED(...) VA_MACRO(MY_OVERLOADED, void, void, __VA_ARGS__)
#define MY_OVERLOADED0(s, t) MacroTest()
#define MY_OVERLOADED1(s, t, a) MacroTest( a)
#define MY_OVERLOADED2(s, t, a, b) MacroTest(a, b)
#define MY_OVERLOADED3(s, t, a, b, c) MacroTest(a, b, c)

int main()
{
    printf("gcc %i.%i.%i\n", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
    printf("MY_OVERLOADED()             : ");
    MY_OVERLOADED();
    printf("MY_OVERLOADED(1)            : ");
    MY_OVERLOADED(1);
    printf("MY_OVERLOADED(11, 22)       : ");
    MY_OVERLOADED(11, 22);
    printf("MY_OVERLOADED(111, 222, 333): ");
    MY_OVERLOADED(111, 222, 333);

    return 0;
}

这应该是扩展(__VAR_ARGS__为空/不存在): MY_OVERLOADED() -> VA_MACRO(MY_OVERLOADED, void, void) 因此,VA_MACRO(MY_OVERLOADED, void, void) -> MacroTest() - Th. Thielemann
我尝试了在线的g++编译器,结果如预期一样。我没有更改任何设置。默认版本是g++ 9.4.0。 - Th. Thielemann

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