如何检查表达式在 C 中是否为常量?
How can I check that an expression is constant in C?
假设我有一个场景,我需要确保我的代码中使用的值是编译时常量(例如,可能是对 P10 rule 2 "fixed loop bounds" 的严格解释)。我如何在 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
的最大值“[可能] 不超过该类型对象的宽度如果省略冒号和表达式,将被指定”(C11 6.7.2.1),这对 expression
的大小设置了非常低的可移植限制(通常可能是 64)。它也可能不是负数。
枚举: 和 enum
要求任何初始化表达式都是整型常量表达式。但是,enum
声明不能嵌入到表达式中(不像 struct
定义),需要有自己的语句。由于枚举器列表中的标识符被添加到周围的范围中,因此我们每次也需要一个新名称。 __COUNTER__
未标准化,因此无法从宏中实现此目的。
案例: 同样,case
行的参数表达式必须是整数常量。但这需要周围的 switch
语句。这并不比 enum
好多少,而且您不想将其隐藏在宏中(因为它会生成真实的语句,即使它们很容易被优化器删除) ).
数组声明: 自 C99 以来,数组大小甚至不必是常量,这意味着它无论如何都不会生成所需的错误。这也是一个需要在周围范围内引入名称的语句,遇到与 enum
.
相同的问题
当然有一些方法可以在可重复的宏中隐藏常量检查,传递值(因此它可以用作表达式),并且不需要语句行或引入额外的标识符?
原来有办法!
虽然本地分配的数组在 C 中允许具有可变长度,但标准明确要求此类数组 not 具有显式初始化程序。我们可以通过给数组一个初始化列表来强制禁用 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
的实际值,但仍考虑其恒定状态;然后它还会忽略数组的大小, returns 仅忽略原始表达式,因此对数组大小的限制 - 必须大于零 - 不需要应用于返回值。 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
,我们至少可以让它接受零。
假设我有一个场景,我需要确保我的代码中使用的值是编译时常量(例如,可能是对 P10 rule 2 "fixed loop bounds" 的严格解释)。我如何在 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
的最大值“[可能] 不超过该类型对象的宽度如果省略冒号和表达式,将被指定”(C11 6.7.2.1),这对expression
的大小设置了非常低的可移植限制(通常可能是 64)。它也可能不是负数。枚举: 和
enum
要求任何初始化表达式都是整型常量表达式。但是,enum
声明不能嵌入到表达式中(不像struct
定义),需要有自己的语句。由于枚举器列表中的标识符被添加到周围的范围中,因此我们每次也需要一个新名称。__COUNTER__
未标准化,因此无法从宏中实现此目的。案例: 同样,
case
行的参数表达式必须是整数常量。但这需要周围的switch
语句。这并不比enum
好多少,而且您不想将其隐藏在宏中(因为它会生成真实的语句,即使它们很容易被优化器删除) ).数组声明: 自 C99 以来,数组大小甚至不必是常量,这意味着它无论如何都不会生成所需的错误。这也是一个需要在周围范围内引入名称的语句,遇到与
enum
. 相同的问题
当然有一些方法可以在可重复的宏中隐藏常量检查,传递值(因此它可以用作表达式),并且不需要语句行或引入额外的标识符?
原来有办法!
虽然本地分配的数组在 C 中允许具有可变长度,但标准明确要求此类数组 not 具有显式初始化程序。我们可以通过给数组一个初始化列表来强制禁用 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
的实际值,但仍考虑其恒定状态;然后它还会忽略数组的大小, returns 仅忽略原始表达式,因此对数组大小的限制 - 必须大于零 - 不需要应用于返回值。 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
,我们至少可以让它接受零。