了解延迟和阻碍宏

Understanding DEFER and OBSTRUCT macros

本文关键字:延迟 了解      更新时间:2023-10-16

我创建了一个小型宏元编程库,它实现了基本的有用结构,如REPEAT(times, x)IF(value, true, false)、元组等。

我的大多数实现都是通过基于可变参数计数或通过计数器重载宏来工作的

// Example:
#define REPEAT_0(x) 
#define REPEAT_1(x) x REPEAT_0(x) 
#define REPEAT_2(x) x REPEAT_1(x)
#define REPEAT_3(x) x REPEAT_2(x)
// ...
// (these defines are generated using an external script)
// ...
#define REPEAT(count, x) CAT(REPEAT_, count)(x)

这工作正常,但我最近遇到了Paul Fultz的一个非常有趣的宏递归实现。

直到延迟表达部分,我毫不费力地理解了他的文章。

但是,我在正确理解DEFEROBSTRUCT的使用方面遇到了很多麻烦。

Paul 实现了一个非常优雅的REPEAT版本,它不需要像这样脚本生成的定义:

#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

DEFEROBSTRUCT和其他实用程序是这样实现的:

#define EMPTY()
#define DEFER(id) id EMPTY()
#define OBSTRUCT(...) __VA_ARGS__ DEFER(EMPTY)()
#define EXPAND(...) __VA_ARGS__
#define A() 123
A() // Expands to 123
DEFER(A)() // Expands to A () because it requires one more scan to fully expand
EXPAND(DEFER(A)()) // Expands to 123, because the EXPAND macro forces another scan

  • 当预处理器展开宏时,结果将被"绘制"到下一次扫描 - 除非发生额外的扫描,否则它不会递归扩展。这是对的吗?

  • EXPAND(...)宏是否强制进行其他扫描?如果是这样,此扫描是否允许宏递归扩展?补间EXPAND(...)DEFER(id)有什么区别?

    • DEFER强制进行两次额外的扫描吗?
  • OBSTRUCT(...)宏呢?它是否强制进行两次额外的扫描?

  • 现在 - 为什么在REPEAT的递归实现中需要OBSTRUCT?为什么DEFEREXPAND在这里不起作用?

DEFER这样的宏的使用,以及一般的复杂C宏学,取决于理解C预处理器如何实际扩展宏表达式。它不仅试图像传统编程语言那样减少所有表达式树,而且它在线令牌流上工作,并且在当前检查流以寻找可能的替换处有一个隐式的"光标"。在扩展过程的任何给定"堆栈帧"中,光标永远不会向后移动,并且一旦令牌在流中传递,就不会再次检查它。

演练DEFER操作的第一个示例:

DEFER(A)()  // cursor starts at the head of the sequence
^            // identifies call to DEFER - push current position
DEFER( A )()  // attempt to expand the argument (nothing to do)
^
// replace occurrences of id in DEFER with A,
// then replace the call to it with the substituted body
A EMPTY() ()  // pop cursor position (start of pasted subsequence)
^              // doesn't find an expansion for A, move on
A EMPTY() ()  // move to next token
^            // EMPTY() is a valid expansion
A  ()         // replace EMPTY() with its body in the same way
^           // continuing...
A ()          // `(` is not a macro, move on
^
A ( )         // `)` is not a macro, move on
^
A ()          // end of sequence, no more expansions
^

DEFER的身体被替换后,光标在身体的"重新扫描"期间移过A,这是第二次也是最后一次尝试扩展该组令牌。一旦光标移过A它就不会在该扩展序列中返回到它,并且由于"重新扫描"位于顶层,因此没有后续扩展序列。

现在考虑相同的表达式,但包装在对EXPAND的调用中:

EXPAND(DEFER(A)())    // cursor starts at the head etc.
^                      // identifies call to EXPAND
EXPAND( DEFER(A)() )  // attempt to expand the argument
^              // this does the same as the first
// example, in a NESTED CONTEXT
// replace occurrences of __VA_ARGS__ in EXPAND with A ()
// then replace the call with the substituted body
A ()          // pop cursor position (start of pasted subsequence)
^              // identifies A, and can expand it this time

由于参数列表在堆叠上下文中展开,并且光标位置恢复到对重新扫描传递的原始调用之前的位置,因此在任何宏的参数列表中放置宏调用 - 即使是实际上什么都不做,如EXPAND- 也会通过扩展光标"免费"额外运行, 通过重置光标在流中的位置,为额外的重新扫描传递提供额外的时间,从而为每个新构造的调用表达式(即将宏名称和括号参数列表推到一起)提供额外的机会被扩展器识别。所以EVAL所做的只是给你 363(3^5+3^4+3^3+3^2+3,有人检查我的数学)免费重新扫描通行证。

因此,根据这一点来解决问题:

  • "涂成蓝色"不是那样工作的(维基中的解释措辞有点误导,尽管它没有错)。宏的名称,如果在该宏中生成,将被永久涂成蓝色(C11 6.10.3.4"[蓝色]令牌不再可用于进一步替换,即使它们稍后被(重新)检查")。DEFER的目的是确保递归调用不会在宏的扩展传递上生成,而是......递 延。。。直到外部重新扫描步骤,此时它不会被涂成蓝色,因为我们不再位于该命名宏中。这就是为什么REPEAT_INDIRECT是类似函数的;这样就可以防止它扩展到任何提到REPEAT名称的内容,只要我们还在REPEAT体内。在原始REPEAT完成后,它至少需要一次免费通行证,以扩大令牌EMPTY间距。
  • 是的,EXPAND强制使用额外的扩展通行证。任何宏调用都会为其参数列表中传递的任何表达式授予一个额外的扩展传递。
    • DEFER的想法是,你不给它传递一个完整的表达式,只传递"函数"部分;它在函数和它的参数列表之间插入一个分隔符,删除一个扩展通道需要花费一个分隔符
    • 因此,EXPANDDEFER之间的区别在于,DEFER强加了额外传递的需要,在您传递的功能被扩展之前;而EXPAND提供了额外的传递。因此,它们是彼此的逆向(并且一起应用,如第一个示例所示,等效于不使用两者的调用)。
  • 是的,OBSTRUCT在您传递的函数被扩展之前需要两次扩展传递。它通过将EMPTY()垫片的扩展DEFER一个(EMPTY EMPTY() ())来实现这一点,在摆脱嵌套EMPTY时刻录第一个光标重置。
  • REPEAT_INDIRECT周围需要OBSTRUCT,因为WHEN扩展(在 true 上)为对EXPAND的调用,这将在我们仍在对REPEAT的调用中烧掉一层间接寻址。如果只有一个层(一个DEFER),嵌套的REPEAT将在我们还在REPEAT的上下文中生成,导致它被涂成蓝色并在那里杀死递归。使用带有OBSTRUCT的两个层意味着嵌套REPEAT不会生成,直到我们到达REPEAT之外的任何宏调用的重新扫描传递,此时我们可以安全地再次生成名称而不会将其涂成蓝色,因为它是从无扩展堆栈中弹出的。

因此,这种递归方法的工作原理是使用大量重新扫描传递(EVAL)的外部源,并将宏名称在其内部的扩展延迟至少一次重新扫描传递,以便它发生在调用堆栈的更下游。REPEAT体的第一次扩展发生在EVAL[363]的重新扫描期间,递归调用被推迟到EVAL[362]的重新扫描,嵌套扩展被推迟......等等。这不是真正的递归,因为它不能形成无限循环,而是依赖于堆栈帧的外部源来烧毁。