【问题标题】:Macro for use in expression while enforcing its arguments to be compile time constants用于表达式的宏,同时将其参数强制为编译时常量
【发布时间】:2025-12-13 21:00:01
【问题描述】:

我正在寻找一种方法来#define 一个宏,该宏将其参数强制为编译时常量,同时可以在表达式中使用。该方法应该在 C90 下工作并且向上兼容 - 如果可能的话,也可以移植到不同的 C++ 变体。此外,内存占用空间为 0 也是可取的。

以编译时最小宏为例。行为应该是:

 #define CT_MIN(CTC_A, CTC B) <<<MAGIC>>>

 int a = 1;
 int b = a + CT_MIN(1,4); /* OK */
 int c = a + CT_MIN(a,4); /* COMPILER DIAGNOSTIC */

为了引发编译诊断,一般来说,我可以模拟一个静态断言,如下所示:

 typedef char ct_check_is_ct_constant[(CTC_A != 0) + 1];

如果 CTC_A 使用编译时间常数(数字)以外的任何内容,则不会编译(或至少会引发一些诊断);但如果 CTC_A 是一个数字,它总是会成功(考虑到范围)。 但是这个断言只能在多语句宏中工作,因此不能用作表达式的一部分。

我猜是这样的:

  #define CT_MIN(CTC_A, CTC_B)                   \
         ( <expr check CTC_A>,                   \
           <expr check CTC_B>,                   \
           (CTC_A) < (CTC_B))? (CTC_A) : (CTC_B) \
         )

但我不知道表达式必须是什么样子,以及是否存在这种情况。

有什么想法吗?

背景:

我通过定制的标题从不太可靠的来源获得了大量常量。这些常量强烈地参数化了我的代码。我想在预处理器和编译时为此常量实现零占用检查,以检查我的假设、环境和我编写正确代码生成器的技能。

【问题讨论】:

  • 注意 VLA 类型。我认为您可能能够定义一个带有位域的结构类型,其大小是根据宏参数定义的,尽管我不相信自己会意识到所有警告并完全正确。
  • @user2357112 :-) 我不完全确定如何使用 VLA 类型来实现 MIN 宏,如果它的参数不是编译时常量,它将在编译时失败。
  • 我的意思是确保您不会意外声明一个认为数组长度始终是编译时常量的 VLA 类型。
  • 啊,我明白了。谢谢。我并不完全了解 VLA 功能(仍然以某种方式坚持使用 C89)。除了答案之外,您能否提供一个不会成为 VLA 的静态断言?​​
  • 必须是标准C89?例如,gcc 的扩展可以让您实现这一目标。

标签: c macros c89 static-assert compile-time-constant


【解决方案1】:

您可以将sizeof 应用于具有单个字段的匿名结构,该字段的类型是本地定义的enum,其值必须是常量整数表达式:

#define CT_MIN(CTC_A, CTC_B)                                               \
    ( sizeof(struct { enum { must_be_constant_expression = CTC_A } x; }),  \
      sizeof(struct { enum { must_be_constant_expression = CTC_B } x; }),  \
      (CTC_A) < (CTC_B) ? (CTC_A) : (CTC_B)                                )

clang 产生的错误信息非常明确:

enumassert.c:32:24: error: expression is not an integer constant expression
    int c = a + CT_MIN(a,4); /* COMPILER DIAGNOSTIC */
                       ^
enumassert.c:17:60: note: expanded from macro 'CT_MIN'
    ( sizeof(struct { enum { must_be_constant_expression = CTC_A } x; }),  \
                                                           ^

此解决方案似乎没有向名称空间添加任何符号。此外,如果您需要处理非整数类型,代价是错误消息的精确度略低,您可以添加这样的转换:

#define CT_MIN(CTC_A, CTC_B)                                               \
    ( sizeof(struct { enum { must_be_constant_expression = (int)(CTC_A) } x; }),  \
      sizeof(struct { enum { must_be_constant_expression = (int)(CTC_B) } x; }),  \
      (CTC_A) < (CTC_B) ? (CTC_A) : (CTC_B)                                )

【讨论】:

  • 看起来确实不错。这种做法是不是给名字空间加点东西,宏用在什么地方?
  • 还有浮点常量怎么样 - 像:'sizeof(struct {enum {XXX = (int) (CTC_A)} x; })' 这样的伎俩吗?溢出也可能会导致问题,对吧?无符号强制转换 - 即 (unsigned int) 是否可以在大浮点数上得到很好的定义?
  • @MarkA.:我认为它甚至不会将符号 must_be_constant_expression 添加到命名空间,因为它的范围在 struct 内,并在下面用不同的值重新定义而不会发出警告。
  • 好的。谢谢!如果您可以确认 answer 给出的解决方案的插件可以完成工作(也许更普遍),您可能想要更新您的答案,那么我愿意接受您的答案。
  • 大浮点变量的强制转换,我认为还不够,因为它无法处理溢出。我用过!运算符从任意数字生成一个小值 - 希望如此。
【解决方案2】:

对于浮点和整数编译时常量的评估,我想我可以使用@chqrlie 的解决方案并稍微升级一下。

检查参数是否为编译时常量的表达式可能是:

  sizeof(struct { enum { must_be_constant_expression = (int) (!(CTC_A))} x; })

(我不确定一元 ! 是否给出了 int 或 CTC_A 类型的东西)

这应该处理大多数事情,可以在编译时合理地完成 MIN 操作。

那么完整的宏就是:

 #define CT_MIN(CTC_A, CTC_B)                                                         \
     ( sizeof(struct { enum { must_be_constant_expression = (int) (!(CTC_A)) } x; }), \
       sizeof(struct { enum { must_be_constant_expression = (int) (!(CTC_B)) } x; }), \
       (CTC_A) < (CTC_B) ? (CTC_A) : (CTC_B)                                          \
     )

【讨论】:

  • ! 运算符的计算结果确实为 int,其值为 01。这个解决方案应该像简单的强制转换一样工作,出于某种原因clang 抱怨如果参数是双精度,它不是编译器时间常数,声称将其折叠为常数是 GNU 扩展。我不明白为什么,但它使这个解决方案无法使用。
  • @chqrlie 查看我对您更新答案的评论。 ...这是编译器错误吗?我在标准中没有理由......
  • 好的。在标准中找到原因:ANSI/ISO 9899:1990 6.5.2.2 枚举说明符:“约束:[...] 枚举常量应为整数常量表达式 [...]”和 6.4 常量表达式:“[. ..] 一个整数常量表达式 [...] 应该只有 [... stuff ...] sizeof 表达式的操作数和作为强制转换的直接操作数的浮动常量。[...]"
  • sizeof 应用于参数会破坏检查它是否为常量表达式的目的。 sizeof(expr) 是一个常量表达式,即使 expr 有副作用...
  • @chqrlie 正确 :-) 无法足够快地删除我过于激动的评论。但是我们可以将结构或数组的初始化列表与这种模式一起工具化吗?在初始化器列表中,我们可以使用任何常量表达式(C90)——那么我们可能会失去向上兼容性——结构初始化器可以是非常量表达式 > C90 标准吗?