C语言中如何解析链式宏?

6
如果我想使用预处理器#define语句来方便定义和计算常量和常用函数并利用较少的RAM开销(与使用const值相比),但是如果多个宏一起使用,我不确定它们如何解析。
我正在设计自己的DateTime代码处理,类似于Linux时间戳,但适用于每秒更新60次的游戏。我更喜欢声明链接值,但不确定硬编码的值是否会更快。
#include <stdint.h>

// my time type, measured in 1/60 of a second.
typedef int64_t DateTime;

// radix for pulling out display values
#define TICKS_PER_SEC  60L
#define SEC_PER_MIN    60L  
#define MIN_PER_HR     60L
#define HRS_PER_DAY    24L
#define DAYS_PER_WEEK   7L
#define WEEKS_PER_YEAR 52L

// defined using previous definitions (I like his style, write once!)
#define TICKS_PER_MIN    TICKS_PER_SEC * SEC_PER_MIN
#define TICKS_PER_HR     TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR
#define TICKS_PER_DAY    TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY
// ... so on, up to years

//hard coded conversion factors.
#define TICKS_PER_MIN_H    3600L      // 60 seconds = 60^2 ticks
#define TICKS_PER_HR_H     216000L    // 60 minutes = 60^3 ticks
#define TICKS_PER_DAY_H    5184000L   // 24 hours   = 60^3 * 24 ticks

// an example macro to get the number of the day of the week
#define sec(t)((t / TICKS_PER_DAY) % DAYS_PER_WEEK)

如果我使用sec(t)宏,它使用TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY定义的TICKS_PER_DAY,那么是否要在调用sec(t)的代码中处处都进行修改?
(t / 5184000L) % 7L)

或者它是否每次都会扩展到以下内容:
(t / (60L * 60L * 60L * 24L)) % 7L)

为什么需要在每个步骤执行额外的乘法指令?这是宏和常量变量之间的平衡取舍,还是我对预处理器的工作原理有误解?

更新:

根据许多有用的答案,将宏链接成常量表达式的最佳设计是用括号包装定义。

1. 正确的操作顺序:

(t / 60 * 60 * 60 * 24) != (t / (60 * 60 * 60 * 24))

2. 通过将常量值分组,鼓励编译器进行常量折叠:

// note parentheses to prevent out-of-order operations
#define TICKS_PER_MIN    (TICKS_PER_SEC * SEC_PER_MIN)
#define TICKS_PER_HR     (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR)
#define TICKS_PER_DAY    (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY)

预处理器不会对此进行优化,但任何值得其盐的编译器都会将其折叠成单个常量值。 - Richard J. Ross III
那么如果我使用像gcc这样的编译器,我应该期望它被视为单个值吗?有没有一种方法可以检查它的行为? - aaroncarsonart
1
这些定义不是“嵌套”的,它们是“链式”扩展:扩展使用宏,其扩展又使用更多的宏。如果存在嵌套,意味着宏的主体可以定义一个宏。 - Kaz
1
你应该从 sec 的定义中去除分号,这样它就能够扩展成一个表达式。否则类似于 sec(x) + 10 的东西是不会工作的。 - Tom Karzes
4个回答

2
预处理器只是进行文本替换。它将评估带有“额外”乘法的第二个表达式。编译器通常会尝试在常数之间优化算术运算,只要它能这样做而不改变答案。
为了最大化其优化机会,您需要注意将常量“相邻”放置,以便它可以看到优化,特别是对于浮点类型。换句话说,如果`t`是一个变量,您会喜欢使用`30 * 20 * t`而不是`30 * t * 20`。

那么根据我的例子,这是否意味着在大多数情况下,编译器会将TICKS_PER_DAY60L * 60L * 60L * 24L优化为5184000L?也就是说,它是否可能像您演示的那样将常量值合并在一起? - aaroncarsonart
1
几乎了解。您应该在宏定义中将整个表达式括在括号中,以便为(60L * 60L * 60L *24L)。以您的方式t / TICKS_PER_DAY由于乘法和除法操作的顺序问题而无法得到您期望的答案。 - Brick

1
宏展开不过是简单的文本替换。在宏被展开后,编译器将解析结果并执行常规优化,其中应包括常量折叠。
然而,这个例子说明了初学者在定义C语言宏时经常犯的一个错误。如果宏的目的是扩展为表达式,则良好的C语言实践要求,如果结果中包含裸露的运算符,则始终应将值包装在括号中。在这个例子中,请看TICKS_PER_DAY的定义:
#define TICKS_PER_DAY    TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY

现在看一下sec(注意分号不应该存在,但我现在会忽略它):
#define sec(t)((t / TICKS_PER_DAY) % DAYS_PER_WEEK);

如果将其实例化为sec(x),它将扩展为:
((x / 60L * 60L * 60L * 24L) % 7L);

这显然不是预期的结果。它只会除以初始值60L,之后剩余的值将被乘以。

修复此问题的正确方法是修复TICKS_PER_DAY的定义,以正确封装其内部操作:

#define TICKS_PER_DAY    (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY)

当然,sec 应该是一个表达式宏,不应包含分号,否则将无法在类似于 sec(x) + 10 的上下文中使用。
#define sec(t)  ((t / TICKS_PER_DAY) % DAYS_PER_WEEK)

现在让我们看看如何使用这些错误修复扩展sec(x)函数:
((x / (60L * 60L * 60L * 24L)) % 7L)

现在这段代码将会按照预期运行。编译器应该会对乘法进行常量折叠,最终只剩下一次除法和一次取模。

编辑:看起来原帖已经添加了缺失的括号。如果没有括号,代码根本无法工作。此外,原帖中多余的分号也已被删除。


有人提到过这个问题,我已经更新了我的代码。也许我应该保留原始错误,并在原始文本下面更新我的帖子,以便其他人可以看到思路的发展? - aaroncarsonart
我不确定该做什么最好。也许在底部添加一个“编辑”注释,说明所做的更改? - Tom Karzes
无论如何,我现在知道括号是任何链接常量宏正常运行的关键部分。谢谢你的解释。 - aaroncarsonart

1
参见gcc预处理器宏文档,具体来说是类对象宏
我认为编译器在这里也起作用。例如,如果我们只考虑预处理器,则应该展开为
(t / (60L * 60L * 60L * 24L)) % 7L)
但是,编译器可能会将其解析为
(t / 5184000L) % 7L)
因为它们是独立的常量,所以执行代码更快/更简单。
注意1:在定义中应使用“(t)”以防止意外扩展/解释。 注意2:另一个最佳实践是避免使用undef,因为这会使代码不那么易读。有关如何影响宏扩展的说明,请参阅此部分(类对象宏)的注释。

更新:来自对象式宏部分:

当预处理器展开宏名称时,宏的扩展将替换宏调用,然后会检查扩展中是否还有更多的宏需要展开。例如:

#define TABLESIZE BUFSIZE #define BUFSIZE 1024 TABLESIZE ==> BUFSIZE ==> 1024 首先展开TABLESIZE以产生BUFSIZE,然后该宏被展开以产生最终结果1024。

请注意,当定义TABLESIZE时,BUFSIZE并未定义。TABLESIZE的‘#define’使用您指定的确切扩展——在本例中为BUFSIZE——并且不会检查它是否也包含宏名称。只有在使用TABLESIZE时,才会扫描其扩展的结果以查找更多的宏名称。

(强调我的)


这正是我一直在寻找但却找不到的文档!谢谢,我会仔细阅读以获得更多的澄清。 - aaroncarsonart
很高兴能帮忙!我还发布了一个更新,提到了你可能感兴趣的关键部分。 - tniles
你的回答似乎最符合我的问题,而且我之前并不知道定义可以“无序”地被定义,但仍然能够正确解析。这让我对使用链接宏来使代码更易读、更易维护和更高效感到自信。 - aaroncarsonart

1
它扩展为:

它扩展为:

(t / (60L * 60L * 60L * 24L)) % 7L)

这是因为宏由预处理器处理,它只是将宏递归地扩展到它们的值(如果必要的话)。
但这并不意味着每次使用sec(t)时整个计算都会重复。这是因为计算发生在编译时。因此您不需要在运行时付出代价。编译器预先计算这样的常量计算,并在生成的代码中使用计算出的值。

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