我们能否使用递归宏?

79

我想知道在C/C++中是否可以有递归宏?如果可以,请提供一个示例。

第二件事:为什么我不能执行下面的代码?我哪里做错了吗?是因为递归宏吗?

# define pr(n) ((n==1)? 1 : pr(n-1))
void main ()
{
    int a=5;
    cout<<"result: "<< pr(5) <<endl;
    getch();
}

C宏是文本宏。如果宏是递归的,你将总是构建一个无限表达式,因为宏除了“用_that_替换_this_”之外什么也做不了。 - Cubic
2
@Cubic:事实上,宏可以做更多的事情。参数引用、文本串联和迭代替换后续定义的宏。但不支持递归。 - Martin York
1
我不确定为什么你想要这样做。如果你打算在编译时进行递归计算,你可能会对变长模板(新C++标准的一个新功能)感兴趣。 - Alexander Oh
1
不,但是模板则是图灵完备的。 - Jasen
6个回答

165

宏不会直接递归扩展,但有解决方法。当预处理器扫描和扩展pr(5)时:

pr(5)
^

它会创建一个禁用的上下文,因此当它再次看到pr时:
((5==1)? 1 : pr(5-1))
             ^

当我们尝试扩展时,它会变成蓝色,无法再进行扩展。但是,我们可以通过使用延迟表达式和一些间接方式来防止我们的宏变成蓝色:

# define EMPTY(...)
# define DEFER(...) __VA_ARGS__ EMPTY()
# define OBSTRUCT(...) __VA_ARGS__ DEFER(EMPTY)()
# define EXPAND(...) __VA_ARGS__

# define pr_id() pr
# define pr(n) ((n==1)? 1 : DEFER(pr_id)()(n-1))

现在它会像这样扩展:

pr(5) // Expands to ((5==1)? 1 : pr_id ()(5 -1))

这很完美,因为pr从来没有被涂成蓝色。我们只需要应用另一个扫描来使其进一步扩展:

EXPAND(pr(5)) // Expands to ((5==1)? 1 : ((5 -1==1)? 1 : pr_id ()(5 -1 -1)))

我们可以应用两种扫描来进一步扩展它:
EXPAND(EXPAND(pr(5))) // Expands to ((5==1)? 1 : ((5 -1==1)? 1 : ((5 -1 -1==1)? 1 : pr_id ()(5 -1 -1 -1))))

然而,由于没有终止条件,我们永远无法应用足够的扫描。我不确定你想要达到什么目的,但如果你对如何创建递归宏感到好奇,这里有一个创建递归重复宏的示例。

首先需要一个宏来应用大量的扫描:

#define EVAL(...)  EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
#define EVAL1(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
#define EVAL2(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__)))
#define EVAL3(...) EVAL4(EVAL4(EVAL4(__VA_ARGS__)))
#define EVAL4(...) EVAL5(EVAL5(EVAL5(__VA_ARGS__)))
#define EVAL5(...) __VA_ARGS__

下一个是concat宏,对于模式匹配非常有用:
#define CAT(a, ...) PRIMITIVE_CAT(a, __VA_ARGS__)
#define PRIMITIVE_CAT(a, ...) a ## __VA_ARGS__

增加和减少计数器:

#define INC(x) PRIMITIVE_CAT(INC_, x)
#define INC_0 1
#define INC_1 2
#define INC_2 3
#define INC_3 4
#define INC_4 5
#define INC_5 6
#define INC_6 7
#define INC_7 8
#define INC_8 9
#define INC_9 9

#define DEC(x) PRIMITIVE_CAT(DEC_, x)
#define DEC_0 0
#define DEC_1 0
#define DEC_2 1
#define DEC_3 2
#define DEC_4 3
#define DEC_5 4
#define DEC_6 5
#define DEC_7 6
#define DEC_8 7
#define DEC_9 8

一些用于条件判断的宏:

#define CHECK_N(x, n, ...) n
#define CHECK(...) CHECK_N(__VA_ARGS__, 0,)

#define NOT(x) CHECK(PRIMITIVE_CAT(NOT_, x))
#define NOT_0 ~, 1,

#define COMPL(b) PRIMITIVE_CAT(COMPL_, b)
#define COMPL_0 1
#define COMPL_1 0

#define BOOL(x) COMPL(NOT(x))

#define IIF(c) PRIMITIVE_CAT(IIF_, c)
#define IIF_0(t, ...) __VA_ARGS__
#define IIF_1(t, ...) t

#define IF(c) IIF(BOOL(c))

#define EAT(...)
#define EXPAND(...) __VA_ARGS__
#define WHEN(c) IF(c)(EXPAND, EAT)

将所有内容综合起来,我们可以创建一个重复宏:
#define REPEAT(count, macro, ...) \
    WHEN(count) \
    ( \
        OBSTRUCT(REPEAT_INDIRECT) () \
        ( \
            DEC(count), macro, __VA_ARGS__ \
        ) \
        OBSTRUCT(macro) \
        ( \
            DEC(count), __VA_ARGS__ \
        ) \
    )
#define REPEAT_INDIRECT() REPEAT

//An example of using this macro
#define M(i, _) i
EVAL(REPEAT(8, M, ~)) // 0 1 2 3 4 5 6 7

所以,是的,通过一些变通方法,你可以在C/C++中拥有递归宏。

2
在使用-std=c99和gcc 4.8.3尝试此操作会导致以下错误:error: 'REPEAT_INDIRECT' undeclared here (not in a function)将REPEAT_INDIRECT的定义移到REPEAT之前也无法解决此问题。 - M.M
预处理器的输出是什么? - Paul Fultz II
1
OBSTRUCT宏在这里没有被展开,因为它在这里没有定义。@Paul在他的原始博客文章中定义它。 - Austin Mullins
3
优雅的解决方案。实际上这就是lambda演算。 - yangwenjin
很棒的解决方案,但是我在VS上无法运行它,似乎EAT不起作用,并且始终会留下REPEAT(0, macro)的最后一次迭代。 - Alexander Torstling

28

你的编译器可能提供了只进行预处理而不实际编译的选项。如果你试图在宏中查找问题,这将非常有用。例如使用g++ -E

> g++ -E recursiveMacro.c

# 1 "recursiveMacro.c"
# 1 "<built-in>"
# 1 "<command line>"
# 1 "recursiveMacro.c"

void main ()
{
    int a=5;
    cout<<"result: "<< ((5==1)? 1 : pr(5 -1)) <<endl;
    getch();
}

正如你所看到的,它并不是递归的。pr(x)只在预处理期间替换一次。记住的重要事情是,所有预处理器所做的只是盲目地用另一个文本字符串替换一个文本字符串,它实际上不会评估像(x == 1)这样的表达式。

你的代码无法编译的原因是,pr(5 -1)没有被预处理器替换,因此它最终作为对未定义函数的调用出现在源代码中。


3
为什么 pr(5-1) 被视为未定义的函数调用?我已经定义了一个宏,所以它应该展开为:((5-1==1)? 1 : pr(5-1-1)) .... 为什么 pr(5-1) 被视为未定义的函数调用?因为在您提供的信息中没有关于 pr 函数的定义。您已经定义了一个宏,但宏展开后只是简单地替换文本,与函数调用不同。宏展开后,表达式将变为 ((5-1==1)? 1 : ((5-1-1==1)? 1 : pr(5-1-1-1))) ,其中两个三元运算符嵌套在一起。 - user1367292
2
@user1367292 不行,你不能使用递归宏。如果它实际上不断地用 pr(x-1) 替换 pr(x),那么它将无限循环 pr(x-1), pr(x-1-1), pr(x-1-1-1) 等等... - verdesmarald
1
veredesmarald -- 那么,你的意思是说“我们不能有递归宏吗?”此外...有没有可用的解决方法来实现这一点? - user1367292
4
抱歉,我不能接受该请求。您在预处理器的上下文中提出的建议没有意义。如果您不断地用某个字符串替换自身加上其他内容,那么如何才能达到递归的基本情况呢? - verdesmarald
veredesmarald -- 谢谢 :-) 明白了。 - user1367292
显示剩余2条评论

20

在 C 或 C++ 中不应该有递归宏定义。

相关的 C++ 标准语言,第 16.3.4 节第 2 段:

如果正在替换的宏名称在预处理器扫描其余源文件的预处理标记时被发现,则不会对其进行替换。此外,如果任何嵌套的替换遇到正在替换的宏名称,则不会对其进行替换。这些未替换的宏名称预处理标记不再可用于进一步的替换,即使它们后来在本应替换该宏名称预处理标记的上下文中被重新检查。

这里存在一些灰色地带。当多个宏相互调用时,这种措辞并不完全表明应该怎么做。对于这种语言律师的问题,C++ 标准存在一个活动问题;请参阅 http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#268

忽略这个语言律师问题,每个编译器厂商都理解其意图:

C 或 C++ 不允许使用递归宏。


11
很可能您无法执行此代码,因为您无法编译它。即使它可以正确编译,它也将始终返回1。您的意思是 (n==1)? 1 : n * pr(n-1)
宏不能递归。根据16.3.4.2章节(感谢Loki Astari),如果在替换列表中找到当前宏,则它将保持不变,因此在定义中的pr不会被更改:
如果在替换列表的这个扫描期间(不包括源文件的其余预处理标记)发现要替换的宏名称,则不进行替换。此外,如果任何嵌套替换遇到要替换的宏名称,则不进行替换。这些未替换的宏名称预处理标记在后续的替换中将不再可用,即使它们在该宏名称预处理标记应替换的上下文中被重新审查。
cout<<"result: "<< pr(5) <<endl;

被预处理器转换为:

cout<<"result: "<< (5==1)? 1 : pr(5-1) <<endl;
在此期间,pr 宏的定义被“丢失”,编译器会显示一个错误,例如“‘pr’ was not declared in this scope (fact)”,因为没有名为pr的函数。
在C++中不鼓励使用宏。你为什么不直接写一个函数呢?
在这种情况下,您甚至可以编写一个模板函数,以便在编译时解析它,并且将行为作为常量值:
template <int n>
int pr() {  pr<n-1>(); }

template <>
int pr<1>() { return 1; }

1
我的意图是了解递归宏。我不是在寻找更好的打印某个值的方法... 我不确定我们是否可以有递归宏。 - user1367292
你的论点是有缺陷的。如果你的宏包含其他宏,那么宏替换算法会重复执行(直到不再进行替换为止)。因此,它可能会执行递归宏。但是语言规范明确禁止这样做,因为一旦宏被替换,它就从后续替换的潜在列表中删除(在同一行内)。 - Martin York

0

简而言之。 真正的递归本身很容易通过在两个名称中复制宏来完成,每个名称都引用另一个名称。但是这个特性的有用性值得怀疑,因为这需要嵌套条件宏才能使递归有限。所有条件宏运算符本质上都是多行的,因为# else和# endif行必须是单独的行(严重的cpp限制),这意味着,按设计来说,条件宏定义是不可能的(因此,递归本身将是无用的)。


-2

在C或C++中,您不能使用递归宏。


好的,我搞清楚了第一个疑问:宏不能递归调用。那么关于我在问题中给出的样例代码中出现的错误呢? - user1367292
你没有说明你遇到了什么错误,但是在pr宏中递归使用的pr不会被展开,可能会导致“未定义函数”错误或类似的错误。 - sth

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