检测C语言表达式是否为常量
Detecting if an expression is constant in C

原始链接: https://nrk.neocities.org/articles/c-constexpr-macro

本文探讨了多种C语言宏实现方案,用于验证表达式是否为编译时常量,如果不是则中断编译,同时理想情况下保留表达式的类型和值。文中考察了几种方法,包括静态复合字面量(C23,支持有限)、`__builtin_constant_p`(GNU扩展,存在类型提升问题)、结合`sizeof`的`static_assert`(C11+,可能改变类型,浮点数会有警告)、结合复合字面量/枚举的`sizeof`(改变类型,兼容C99/C89,会有警告)以及逗号运算符(会有大量警告)。这些方案常常难以兼顾类型保持、浮点数常量和编译器特有的行为。本文总结认为,要实现一个完美、无警告且符合标准的方案极具挑战性,尤其是在希望避免使用编译器扩展和大量警告的情况下。

Hacker News 最新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 检测C语言中表达式是否为常量 (nrk.neocities.org) ingve 1小时前 7 分 | 隐藏 | 过去 | 收藏 | 讨论 加入我们,参加6月16-17日在旧金山举办的AI创业学校! 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请YC | 联系我们 搜索:

原文
Detecting if an expression is constant in C

21 Apr 2025

Here's a fun little language puzzle: implement a macro that takes an expression as an argument and:

  • Verifies that the expression is a constant expression (i.e, known at compile time), otherwise aborts the compilation.
  • "Returns" back the same value.
  • Optionally, the returned value has the same type as the original expression.

There's a number of ways to solve this, depending on which C standard you're using and whether compiler extensions are allowed or not. Here are a few I've come across along with some pros and cons.

static compound literals

If you're using C23 or later then you can specify a storage duration for compound literals combined with typeof (also standardized in C23).

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

This keeps the same type, and since initializers of static storage duration need to be constant expression, the compiler will ensure that x is a constant expression.

Cons:

  • Requires C23, which isn't widely supported as of this writing.
  • Clang (v20) doesn't seem to support static in compound literals just yet (GCC v14.2 seems to work).

__builtin_constant_p

If using GNU extensions is not a problem, then you can use __builtin_constant_p, which returns true when an expression is constant.

__attribute((error("not constant"))) int notconst(void);

#define C(x) ((__typeof__(x)) \
    ((x) + __builtin_constant_p(x) ? 0 : notconst()) \
)

When __builtin_constant_p returns true, it adds 0 to the value, leaving it unchanged. Otherwise, it calls a dummy non-existent function declared with the error attribute, causing a compilation error.

However, since the addition will end up performing the usual integer promotion, the type of the result may be different. That's why there's a __typeof__(x) cast, to keep the same type.

Cons:

static_assert

This trick was shown to me by constxd in an IRC channel and uses C11+ static_assert to ensure the expression is static. But how do we "return" the expression back? Well... with a bit of sizeof + anon struct magic:

#define C(x) ((x) + 0*sizeof( \
    struct { _Static_assert((int)(x) || 1, ""); char tmp; } \
))

This looks very wonky. How is a static_assert allowed inside a struct declaration? It's because the standard classifies static_assert as a declaration which declares nothing (don't ask me why). So syntactically, you can just put it inside a struct.

Cons:

  • May change the type of the expression due to the addition, which is subject to the typical integer promotion rules. (Though see the comma operator section for a solution).
  • Standard says the expression of static_assert needs to be an integer constant expression, though gcc and clang seems to accept floating point expression as well, but emits warnings. (Note: the immediate int cast makes floating constant e.g 1.1 work, but not things like 1.1 + 2.2, without emitting warnings that is).

sizeof + compound literal with array type

This uses a similar trick as the above but instead of static_assert, it uses a compound literal array type to ensure that the expression is constant:

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

A couple things to note:

  1. Since C99, arrays (and array types) can be of variable length. But compound literals do not accept variable-length types, which does the verification for us.
  2. Standard C (unfortunately) forbids zero-length arrays, hence the || 1.
  3. Array sizes beyond a certain limit cannot be declared (even if no storage is being allocated). On my 64 bit PC, gcc refuses sizes above PTRDIFF_MAX (263-1) bytes, and clang is even more conservative and rejects sizes above 61 bits. Aside from supporting 0, the || 1 also clamps down the array size to 1.

The only advantage of this approach over static_assert is that it doesn't require C11 and can be used in C99. Other than that it inherits all the problems that comes with static_assert:

  • The type may change.
  • Doesn't support floating expression (and unlike static_assert gcc refuses to compile floating expression completely).

sizeof + enum constant

Because enum constants are required to be integer constant expression, we can use them instead of compound literal:

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

However, there's one glaring issue with this: unlike the compound literal, the enum constant "leaks out". Meaning that you cannot use this macro more than once. You could try to use pre-processor concat to append the line number (__LINE__) but then you can't use this macro more than once on the same line.

Here's a neat (or cursed) little solution I came up with: declare the enum inside a function parameter to give it "scope":

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

This works. But both gcc and clang warn about the enum being anonymous... even though that's exactly what I wanted to do. And this cannot be silenced with #pragma since it's a macro, so the warning occurs at the location where the macro is invoked.

Practically there's not much reason to use this, but it's C89 compatible, if that's something you care about. Cons:

  • The type may change.
  • Doesn't support floating expression.
  • Both gcc and clang warn about enum being anonymous for every macro invocation.

Comma operator

All the macros that are using (x) + 0*sizeof(...) trick suffer from the type potentially changing. There's a simple and elegant solution to this, put the sizeof in a separate expression and ignore it with the comma operator:

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

But the problem is that you will get a bunch of warnings about "left-hand operand of comma expression has no effect", even though that's precisely the desired effect.

GCC wonkiness

Initially, I wasn't using error attribute for my __builtin_constant_p solution, but rather I was using a negative sized array (wrapped inside a struct where VLAs are forbidden) to trigger the compilation error:

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

Clang, expectedly errors when the array size is -1. GCC however eats it up, and lets you off the hook with just a warning (double yikes). Hence the error attribute instead (suggested by Arsen) which should be robust against this type of wonkiness.

Conclusion

This turned out to be much bigger rabbit hole than I initially expected. The noisy warnings were also an annoyance, but because I wanted to use this macro in a library, simply turning off the warning wasn't an option for me. And I'd rather keep the library warning free instead of telling the users to switch warnings off.

I'd be interested in knowing if there's any other (hopefully better) solutions that I missed.


UPDATE: u/P-p-H-d points out this solution that uses _Generic, ternary operator and null-pointer constant rules to determine an integer constant expression. Requires C11 and only works for integer, doesn't work for floating point expression unfortunately.

Tags: [ c ]




联系我们 contact @ memedata.com