【问题标题】:How can I check that an expression is constant in C? [duplicate]如何检查表达式在 C 中是否为常量? [复制]
【发布时间】:2016-02-24 09:32:29
【问题描述】:

假设我有一个场景,我需要确保我的代码中使用的值是编译时常量(例如,可能是对 P10 rule 2“固定循环边界”的严格解释)。如何在 C 语言级别强制执行此操作?

C 在语言级别支持整数常量表达式 的概念。必须有可能找到一种方法来利用这一点,以便只有符合本规范的值才能在表达式中使用,对吧?例如:

for (int i = 0; i < assert_constant(10); ++i) {...

一些部分解决方案不够通用,无法在多种情况下使用:

  • 位域:在 C11 之前在 C 中实现 static_assert 的经典策略是 use a bitfield whose value would be illegal when a condition failed

    struct { int _:(expression); }
    

    虽然这可以很容易地包装为表达式的一部分,但它根本不通用 - expression "[may] 的最大值不会超过指定类型的对象的宽度是否省略了冒号和表达式”(C11 6.7.2.1),这对 expression 的大小设置了非常低的便携限制(通常可能为 64)。它也可能不是负面的。

  • 枚举:enum 要求任何初始化表达式都是整数常量表达式。但是,enum 声明不能嵌入到表达式中(与 struct 定义不同),需要它自己的语句。由于枚举器列表中的标识符被添加到周围的作用域中,因此我们每次也需要一个新名称。 __COUNTER__ 没有标准化,因此无法从宏中实现。

  • 案例:同样,case 行的参数表达式必须是整数常量。但这需要围绕switch 声明。这并不比enum 好很多,而且你不想隐藏在宏中(因为它会生成真实的语句,即使它们很容易被优化器删除)。

  • 数组声明: 从 C99 开始,数组大小甚至不必是常数,这意味着它无论如何都不会产生所需的错误。这也是一个需要在周围范围中引入名称的语句,遇到与enum 相同的问题。

肯定有某种方法可以在可重复的宏中隐藏常量检查,传递值(因此它可以用作表达式),并且不需要语句行或引入额外的标识符?

【问题讨论】:

  • 另见this SO post?
  • @quantdev sigh 真的以为我已经详尽地搜索了这个主题......

标签: c compile-time constant-expression


【解决方案1】:

原来有办法!

虽然在 C 中允许本地分配的数组具有可变长度,但标准明确要求此类数组具有显式初始化程序。我们可以通过给数组一个 initializer-list 来强制禁用 VLA 语言特性,这将强制数组大小为整数常量表达式(编译时常量):

int arr[(expression)] = { 0 };

初始化器的内容无关紧要; { 0 } 将始终有效。

这仍然略逊于enum 解决方案,因为它需要一个语句并引入一个名称。但是,与枚举不同,数组可以匿名(作为复合文字):

(int[expression]){ 0 }

由于复合文字有一个初始化器作为语法的一部分,所以它不可能成为 VLA,所以这仍然保证要求 expression 是一个编译时常量。

最后,因为匿名数组是表达式,我们可以将它们传递给sizeof,这为我们提供了一种传递expression 原始值的方法:

sizeof((char[expression]){ 0 })

这有一个额外的好处是保证数组永远不会在运行时被分配。

最后,稍微总结一下,我们甚至可以处理零值或负值:

sizeof((char[(expression)*0+1]){ 0 }) * 0 + (expression)

这在设置数组大小时忽略了expression的实际(始终为1),但仍考虑其恒定状态;然后它也忽略数组的大小并仅返回原始表达式,因此对数组大小的限制 - 必须大于零 - 不需要应用于返回值。 expression 是重复的,但这就是宏的用途(如果编译,它不会被重新计算,因为 a. 它是一个常量,并且 b. 第一次使用在 sizeof 内)。因此:

#define assert_constant(X) (sizeof((char[(X)*0+1]){ 0 }) * 0 + (X))

对于加分,我们可以使用非常相似的技术来实现static_switch表达式,通过将数组大小与 C11 的 _Generic 结合起来(这可能没有太多实际用途,但可能会替换一些不流行的嵌套三元组):

#define static_switch(VAL, ...) _Generic(&(char[(VAL) + 1]){0}, __VA_ARGS__)
#define static_case(N) char(*)[(N) + 1]

char * x = static_switch(3,
             static_case(0): "zero",
             static_case(1): "one",
             static_case(2): "two",
             default: "lots");
printf("result: '%s'\n", x); //result: 'lots'

(我们使用数组的地址来生成一个明确的指向数组的指针类型,而不是让实现来决定 _Generic 是否将数组提升为指针;截至 2016 年 4 月,这种歧义已在语言中得到修复DR 481 及其随后的 TC。)

这比assert_constant 稍微严格一些,因为它不允许负值。不过,通过将+1 放在控制表达式 所有 case 值中,我们至少可以让它接受零。

【讨论】:

  • 我无法让它在我的编译器上工作(xcc,它类似于“gcc”,但适用于特定供应商的嵌入式 MCU)。如果我使用(X)*0(X)-(X),编译器似乎很聪明,不会对其进行评估。相反,我使用了:#define ASSERT_CONSTANT(X) (sizeof((char[((int)(X)&amp;1)+1]){ 0 })*0 + (X)),它适用于浮点数和整数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-28
  • 2015-02-16
  • 2017-01-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多