C 语言中检测表达式是否为常量表达式

2025 年 4 月 21 日

这是一个有趣的小语言难题:实现一个宏,它接受一个表达式作为参数,并且:

有很多方法可以解决这个问题,具体取决于你使用的 C 标准以及是否允许编译器扩展。以下是我遇到的一些方法,以及它们的优缺点。

static 复合字面量

如果你使用的是 C23 或更高版本,那么你可以为复合字面量指定存储持续时间,并结合 typeof(也在 C23 中标准化)。

#define C(x) ( (static typeof(x)){x} )

这保持了相同的类型,并且由于 static 存储持续时间的初始化器需要是常量表达式,因此编译器将确保 x 是一个常量表达式。

缺点:

__builtin_constant_p

如果使用 GNU 扩展没有问题,那么你可以使用 __builtin_constant_p,当表达式为常量时,它返回 true。

__attribute((error("not constant"))) int notconst(void);
#define C(x) ((__typeof__(x)) \
  ((x) + __builtin_constant_p(x) ? 0 : notconst()) \
)

__builtin_constant_p 返回 true 时,它将 0 添加到该值,使其保持不变。否则,它会调用一个使用 error 属性声明的虚拟不存在的函数,从而导致编译错误。

但是,由于加法最终会执行通常的整数提升,因此结果的类型可能会有所不同。这就是为什么有一个 __typeof__(x) 强制转换,以保持相同的类型。

缺点:

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 允许在结构声明中?这是因为标准将 static_assert 分类为不声明任何内容的声明(别问我为什么)。所以从语法上讲,你可以把它放在一个结构体里面。

缺点:

sizeof + 具有数组类型的复合字面量

这使用与上述类似的技巧,但不是使用 static_assert,而是使用复合字面量数组类型来确保表达式是常量:

#define C(x) ( (x) + 0*sizeof( (char [(int)(x) || 1]){0} ) )

需要注意的几点:

  1. 自 C99 以来,数组(和数组类型)可以是可变长度的。但是复合字面量不接受可变长度类型,这为我们进行了验证。
  2. 标准 C(不幸的是)禁止零长度数组,因此是 || 1
  3. 超过一定限制的数组大小无法声明(即使没有分配存储)。在我的 64 位 PC 上,gcc 拒绝大于 PTRDIFF_MAX (263-1) 字节的大小,而 clang 甚至更保守,拒绝大于 61 位的尺寸。除了支持 0 之外,|| 1 还会将数组大小限制为 1。

static_assert 相比,这种方法的唯一优点是它不需要 C11,可以在 C99 中使用。除此之外,它继承了 static_assert 带来的所有问题:

sizeof + 枚举常量

因为枚举常量必须是整数常量表达式,所以我们可以使用它们来代替复合字面量:

#define C(x) ( (x) + 0*sizeof( enum { tmp = (int)(x) } ) )

但是,这存在一个明显的问题:与复合字面量不同,枚举常量会“泄露”。这意味着你不能多次使用这个宏。你可以尝试使用预处理器连接来附加行号(__LINE__),但是你不能在同一行上多次使用此宏。

这是我想出的一个巧妙(或诅咒的)小解决方案:在函数参数中声明枚举,使其具有“作用域”:

#define C(x) ( (x) + 0*sizeof(void (*)(enum { tmp = (int)(x) })) )

这行得通。但是 gcc 和 clang 都会警告枚举是匿名的……即使这 正是 我想要做的。而且这不能用 #pragma 静默,因为它是一个宏,所以警告发生在调用宏的位置。

实际上没有太多理由使用它,但它是 C89 兼容的,如果你关心的话。

缺点:

逗号运算符

所有使用 (x) + 0*sizeof(...) 技巧的宏都存在类型可能更改的问题。对此有一个简单而优雅的解决方案,将 sizeof 放在一个单独的表达式中,并用逗号运算符 忽略 它:

#define C(x) (sizeof(...), (x))

但问题是,你会收到一堆关于“逗号表达式的左侧操作数不起作用”的警告,即使这 正是 想要的效果。

GCC 的怪异之处

最初,我没有为我的 __builtin_constant_p 解决方案使用 error 属性,而是使用负大小的数组(包装在禁止 VLA 的结构中)来触发编译错误:

#define C(x) ((__typeof__(x)) ((x) + 0*sizeof( \
  struct { char tmp[__builtin_constant_p(x) ? 1 : -1]; } \
)))

Clang 在数组大小为 -1 时如预期般报错。 然而,GCC 将其吞噬,并仅用警告让你脱身(双重叹息)。 因此,Arsen 建议使用 error 属性代替,它应该可以防止这种类型的怪异现象。

结论

事实证明,这是一个比我最初预期的更大的兔子洞。恼人的警告也很烦人,但因为我想在库中使用这个宏,所以简单地关闭警告对我来说不是一个选择。 我宁愿保持库没有警告,也不愿告诉用户关闭警告。

如果有我遗漏的任何其他(希望更好的)解决方案,我将很感兴趣。

更新u/P-p-H-d 指出了此解决方案,该解决方案使用 _Generic、三元运算符和空指针常量规则来确定整数常量表达式。 需要 C11,并且仅适用于整数,不幸的是不适用于浮点表达式。

标签:[c]