7

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

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

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

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

  • 位域:static_assert在 C11 之前的 C中实现的经典策略是使用一个位域,当条件失败时其值将是非法的

    struct { int _:(expression); }
    

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

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

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

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

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

4

1 回答 1

9

原来有办法!

尽管在 C 中允许本地分配的数组具有可变长度,但标准明确要求此类数组没有显式初始化程序。我们可以通过给数组一个初始化列表来强制禁用 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. 第一次使用在 a 内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这比不接受负值的限制性稍强一些。但是,通过将 a+1放入控制表达式所有 case 值中,我们至少可以让它接受零。

于 2015-11-22T03:43:07.763 回答