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 immediateint
cast makes floating constant e.g1.1
work, but not things like1.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:
- 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.
- Standard C (unfortunately) forbids zero-length arrays, hence the
|| 1
. - 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 supporting0
, 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 ]