如何在C语言中检查一个表达式是否为常量?

9
我有一个场景,需要确保我的代码中使用的值是编译时常量(例如,可能是对P10 rule 2“固定循环边界”的严格解释)。在C语言中,如何在语言层面上强制执行这一点?
C语言支持语言级别的整数常量表达式。必须有可能找到一种方法利用这一点,以便只有符合此规范的值可以用于表达式,对吧?例如:
for (int i = 0; i < assert_constant(10); ++i) {...

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

  • Bitfields: a classic strategy for implementing static_assert in C prior to C11 was to use a bitfield whose value would be illegal when a condition failed:

    struct { int _:(expression); }
    

    While this could be easily wrapped for use as part of an expression, it isn't general at all - the maximum value of expression "[may] not exceed the width of an object of the type that would be specified were the colon and expression omitted" (C11 6.7.2.1), which places a very low portable limit on the magnitude of expression (generally likely to be 64). It also may not be negative.

  • Enumerations: an enum demands that any initializing expressions be integer constant expressions. However, an enum declaration cannot be embedded into an expression (unlike a struct definition), requiring its own statement. Since the identifiers in the enumerator list are added to the surrounding scope, we also need a new name each time. __COUNTER__ isn't standardized, so there's no way to achieve this from within a macro.

  • Case: again, the argument expression to a case line must be an integer constant. But this requires a surrounding switch statement. This isn't a whole lot better than enum, and it's the kind of thing you don't want to hide inside a macro (since it will generate real statements, even if they're easy for the optimizer to remove).

  • Array declaration: since C99, the array size doesn't even have to be a constant, meaning it won't generate the desired error anyway. It also again is a statement that requires introducing a name into the surrounding scope, suffering from the same problems as enum.

肯定有一种方法可以隐藏宏中的常量检查,使其可重复,通过(因此可以用作表达式),而不需要语句行或引入额外的标识符?


1
还可以参考这篇SO帖子 - quantdev
@quantdev 叹气 真的以为我已经彻底搜索了这个主题... - Alex Celeste
1个回答

11

原来还有一种方法!

虽然在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)

这个方法在设置数组大小时忽略了表达式的实际(它将始终为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'

为了产生明确的数组指针类型,我们采用数组地址的方式,而不是让实现决定是否将数组升级为指针;截至2016年4月,DR 481及其随后的TC已经解决了这种模糊性。

assert_constant相比稍微有些限制,因为它不接受负值。通过在控制表达式和所有 case 值中都加上+1,我们至少可以使其接受零。


1
我在我的编译器(xcc,类似于"gcc"但是针对特定供应商的嵌入式MCU)上无法让它工作。如果我使用(X)*0(X)-(X),编译器似乎很聪明,不会对其进行评估。相反,我使用了: #define ASSERT_CONSTANT(X) (sizeof((char[((int)(X)&1)+1]){ 0 })*0 + (X)),这将适用于浮点数和整数。 - eresonance
@eresonance,&可能会引起麻烦,对吗?这个对你也适用吗:(sizeof((char[((0+(x)) && 1) + 1]){0}) * 0 + (x))(必须添加0,否则某些编译器会对x=3*4的整数运算在布尔上下文中发出警告)。 - undefined
@eresonance 或者更好的是(也适用于C89)__pragma(warning(suppress: 4116)) 前缀加上 (sizeof(struct { int :(((0+(x)) && 1) + 1); }) * 0 + (x)),如果定义了 _MSC_VER。 - undefined
我认为&1是可以的,因为你只是想要确保给定X的合适常量输入,确保数组大小大于0。无论是1还是2都不太重要。我认为...&&1也可以正常工作?但我不明白你的例子中(0+(x))是用来做什么的。 - undefined

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