C 语言中检测表达式是否为常量
C 语言中检测表达式是否为常量
2025年4月21日
这里有一个有趣的语言难题:实现一个宏,它接受一个(整数或浮点)表达式作为参数,并且:
- 验证表达式是否为常量表达式(即,在编译时已知),否则中止编译。
- “返回”相同的值。
- 可选地,返回的值具有与原始表达式相同的类型。
有很多方法可以解决这个问题,具体取决于你使用的 C 标准以及是否允许编译器扩展。以下是我遇到的一些方法以及一些优点和缺点。
constexpr
复合字面量
如果你使用的是 C23 或更高版本,则可以为复合字面量指定存储持续时间。结合 typeof
和 constexpr
,两者也在 C23 中标准化,你可以使用以下代码:
#define C(x) ( (constexpr typeof(x)){x} )
这保持了类型不变,并且由于 constexpr
存储持续时间的初始化器需要是常量表达式,因此编译器将确保 x
是常量表达式。
缺点:
- 需要 C23,截至撰写本文时,尚未得到广泛支持。
Clang
(v20)似乎还不支持复合字面量中的存储持续时间(GCC
v14.2 似乎可以工作)。
本文的早期版本使用 static
代替 constexpr
。由于 static
存储持续时间要求初始化器为常量表达式,因此编译器会确保它。
但是,constexpr
在这里具有稍微更好的语义,因为与 static
不同,constexpr
强制要求_结果_表达式(不仅仅是初始化器)也将是一个常量表达式。
__builtin_constant_p

如果使用 GNU
扩展没有问题,那么你可以使用 __builtin_constant_p
,当表达式为常量时,它返回 true,并结合 __builtin_choose_expr
进行选择。
__attribute((error("not constant"))) int notconst(void);
#define C(x) __builtin_choose_expr(__builtin_constant_p(x), (x), notconst())
当 __builtin_constant_p
返回 true 时,__builtin_choose_expr
选择第一个表达式。否则,它会调用一个使用 error
attribute声明的虚拟不存在的函数,从而导致编译错误。
与三元运算符不同,__builtin_choose_expr
不受类型提升规则的约束,因此保持表达式的类型不变。
缺点:
- 需要
GNU
扩展。
static_assert

这个技巧是由 constxd
在 IRC
频道中向我展示的,它使用 C11+
static_assert
来确保表达式是静态的。但是我们如何“返回”表达式呢?好吧...使用一点 sizeof
+ 匿名 struct
魔法:
#define C(x) ((x) + 0*sizeof( \
struct { _Static_assert((int)(x) || 1, ""); char tmp; } \
))
这看起来很古怪。为什么 static_assert
允许在 struct
声明中使用?这是因为标准将 static_assert
分类为声明,该声明不声明任何内容(不要问我为什么)。因此,从语法上讲,你可以将其放入 struct
中。
0*sizeof(...)
的加法实际上是一个空操作,使值保持不变,但类型可能会因提升而改变。
缺点:
- 由于加法,可能会更改表达式的类型,这受典型的整数提升规则的约束。(但请参阅逗号运算符部分中的解决方案)。
- 标准规定
static_assert
的表达式需要是整数常量表达式,尽管gcc
和clang
似乎也接受浮点表达式,但会发出警告。(注意:直接的int
转换使_浮点常量_例如1.1
可以工作,但不会像1.1 + 2.2
这样的表达式工作,而不会发出警告)。
sizeof
+ 具有数组类型的复合字面量
这使用与上述类似的技巧,但它没有使用 static_assert
,而是使用复合字面量数组类型来确保表达式是常量:
#define C(x) ( (x) + 0*sizeof( (char [(int)(x) || 1]){0} ) )
需要注意以下几点:
- 自
C99
以来,数组(和数组类型)可以是可变长度的。但是复合字面量不接受可变长度类型,这为我们进行了验证。 - 标准
C
(不幸的是)禁止零长度数组,因此使用|| 1
。 - 超过一定限制的数组大小无法声明(即使没有分配存储空间)。在我的 64 位
PC
上,gcc
拒绝大于PTRDIFF_MAX
(263-1) 字节的大小,而clang
甚至更保守,拒绝大于 61 位的 大小。除了支持0
之外,|| 1
还将数组大小限制为 1。
与 static_assert
相比,这种方法的唯一优点是它不需要 C11
,并且可以在 C99
中使用。除此之外,它继承了 static_assert
的所有问题:
- 类型可能会更改。
- 不支持浮点表达式(并且与
static_assert
不同,gcc
完全拒绝编译浮点表达式)。
sizeof
+ enum
常量
因为 enum
常量必须是整数常量表达式,所以我们可以使用它们而不是复合字面量:
#define C(x) ( (x) + 0*sizeof( enum { tmp = (int)(x) } ) )
但是,这存在一个明显的问题:与复合字面量不同,enum
常量会“泄漏出去”。这意味着你不能多次使用此宏。你可以尝试使用预处理器连接来附加行号(__LINE__
),但是你不能在同一行上多次使用此宏。
这是我想出的一个巧妙(或诅咒)的小解决方案:在函数参数中声明 enum
,使其具有“作用域”:
#define C(x) ( (x) + 0*sizeof(void (*)(enum { tmp = (int)(x) })) )
这有效。但是 gcc
和 clang
都会警告 enum
是匿名的……即使这_正是_我想要做的。并且无法使用 #pragma
静默此警告,因为它是一个宏,因此警告发生在调用宏的位置。
实际上,没有太多理由使用它,但它是 C89
兼容的,如果你关心这一点的话。
缺点:
- 类型可能会更改。
- 不支持浮点表达式。
gcc
和clang
都会警告enum
对于每个宏调用都是匿名的。
逗号运算符
所有使用 (x) + 0*sizeof(...)
技巧的宏都存在类型可能改变的问题。有一个简单而优雅的解决方案,将 sizeof
放在一个单独的表达式中,并使用逗号运算符_忽略_它:
#define C(x) (sizeof(...), (x))
但问题是你将收到一堆关于“逗号表达式的左侧操作数无效”的警告,即使这_正是_想要的效果。
更新:多位读者指出,将 sizeof
的结果强制转换为 void
可以消除未使用的警告(duh!)。
GCC
的怪异之处
最初,我没有为我的 __builtin_constant_p
解决方案使用 error
属性,而是使用负大小的数组(包装在禁止 VLA
的结构中)来触发编译错误:
#define C(x) ((__typeof__(x)) ((x) + 0*sizeof( \
struct { char tmp[__builtin_constant_p(x) ? 1 : -1]; } \
)))
正如预期的那样,当数组大小为 -1
时,Clang
会报错。然而,GCC
却接受了它,并且只用一个警告就放过了你(双重糟糕)。因此,改用 error
属性(由 Arsen
建议),它应该可以抵御这种类型的怪异之处。
结论
事实证明,这比我最初预期的要大得多。嘈杂的警告也很烦人,但因为我想在库中使用此宏,所以简单地关闭警告对我来说不是一个选择。而且我宁愿保持库没有警告,而不是告诉用户关闭警告。
如果有任何其他(希望更好的)解决方案,我很想知道。
更新:[u/P-p-H-d](https://nrk.neocities.org/articles/<https:/old.reddit.com/r/C_Programming/comments/1k4jh8u/detecting_if_an_expression_is_constant_in_c/moaqh3s/>)
指出了这个解决方案,它使用 _Generic
、三元运算符和空指针常量规则来确定整数常量表达式。需要 C11
并且仅适用于整数,不幸的是不适用于浮点表达式。
标签: [ c ]