Here's a fun little language puzzle: implement a macro that takes an (integer or floating) expression as an argument and:
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.
If you're using C23 or later then you can specify a storage duration for
compound literals.
Combined with typeof
and constexpr
, both of which were also
standardized in C23, you can use the following:
#define C(x) ( (constexpr typeof(x)){x} )
This keeps the type intact, and since initializers of constexpr
storage
duration need to be constant expression, the compiler will ensure that x
is a
constant expression.
Cons:
Earlier version of this article used static
instead of constexpr
.
Since static
storage duration requires initializer to be constant expression,
the compiler would ensure it.
However, constexpr
has slightly better semantics here because unlike static
,
constexpr
mandates that the resulting expression (not merely the
initializer) will also be a constant expression.
If using GNU extensions is not a problem, then you can use
__builtin_constant_p
, which returns true when an expression is constant
along with __builtin_choose_expr
for selection.
__attribute((error("not constant"))) int notconst(void);
#define C(x) __builtin_choose_expr(__builtin_constant_p(x), (x), notconst())
When __builtin_constant_p
returns true, __builtin_choose_expr
picks the
first expression.
Otherwise, it calls a dummy non-existent function declared with the
error
attribute, causing a compilation error.
Unlike ternary operator, __builtin_choose_expr
isn't subject to the type
promotion rules and thus keeps the type of the expression intact.
Cons:
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.
The addition by 0*sizeof(...)
is effectively a no-op, leaving the value
unchanged, but the type may change due to promotion.
Cons:
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).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
.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
:
static_assert
gcc refuses to
compile floating expression completely).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:
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.
UPDATE: Multiple readers have pointed out that casting the result of sizeof
to void
silences the unused warnings (duh!).
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.
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 ]