9

Say I have a scenario where I need to ensure that a value used in my code is a compile-time constant (e.g. perhaps a draconian interpretation of P10 rule 2 "fixed loop bounds"). How can I enforce this at the language-level in C?

C supports the notion of integer constant expression at the language-level. It must be possible to work out a way to take advantage of this so that only values complying with this specification can be used in expressions, right? e.g.:

for (int i = 0; i < assert_constant(10); ++i) {...

Some partial solutions that aren't really general enough to be useful in multiple situations:

  • Bitfields: a classic strategy for implementing static_assert in C prior to C11 was to use a bitfield whose value would be illegal when a condition failed:

    struct { int _:(expression); }
    

    While this could be easily wrapped for use as part of an expression, it isn't general at all - the maximum value of expression "[may] not exceed the width of an object of the type that would be specified were the colon and expression omitted" (C11 6.7.2.1), which places a very low portable limit on the magnitude of expression (generally likely to be 64). It also may not be negative.

  • Enumerations: an enum demands that any initializing expressions be integer constant expressions. However, an enum declaration cannot be embedded into an expression (unlike a struct definition), requiring its own statement. Since the identifiers in the enumerator list are added to the surrounding scope, we also need a new name each time. __COUNTER__ isn't standardized, so there's no way to achieve this from within a macro.

  • Case: again, the argument expression to a case line must be an integer constant. But this requires a surrounding switch statement. This isn't a whole lot better than enum, and it's the kind of thing you don't want to hide inside a macro (since it will generate real statements, even if they're easy for the optimizer to remove).

  • Array declaration: since C99, the array size doesn't even have to be a constant, meaning it won't generate the desired error anyway. It also again is a statement that requires introducing a name into the surrounding scope, suffering from the same problems as enum.

Surely there's some way to hide the constant check in a macro that's repeatable, passes the value through (so it can be used as an expression), and doesn't require a statement line or introduce extra identifiers?

Community
  • 1
  • 1
Alex Celeste
  • 12,824
  • 10
  • 46
  • 89
  • 1
    Also see [this SO post](http://stackoverflow.com/questions/9274532/how-to-check-if-a-parameter-is-an-integral-constant-expression-in-a-c-preprocess?rq=1) ? – quantdev Nov 22 '15 at 03:47
  • @quantdev *sigh* really thought I'd searched exhaustively for this topic... – Alex Celeste Nov 22 '15 at 04:27

1 Answers1

10

Turns out there is a way!

Although locally-allocated arrays are allowed to have variable length in C, the standard explicitly requires that such arrays not have an explicit initializer. We can forcibly disable the VLA language feature by giving an array an initializer-list, which will force the array size to be an integer constant expression (compile-time constant):

int arr[(expression)] = { 0 };

The content of the initializer doesn't matter; { 0 } will always work.

This is still slightly inferior to the enum solution because it requires a statement and introduces a name. But, unlike enumerations, arrays can be made anonymous (as compound literals):

(int[expression]){ 0 }

Since a compound literal has an initializer as part of the syntax, there's no way for it to ever be a VLA, so this is still guaranteed to require expression to be a compile-time constant.

Finally, because anonymous arrays are expressions, we can pass them to sizeof which gives us a way to passthrough the original value of expression:

sizeof((char[expression]){ 0 })

This has the added bonus of guaranteeing that the array never gets allocated at runtime.

Finally, with a bit more wrapping up, we can even handle zero or negative values:

sizeof((char[(expression)*0+1]){ 0 }) * 0 + (expression)

This disregards the actual value of expression when setting the array size (which will always be 1), but still considers its constant status; it then also disregards the size of the array and returns only the original expression, so the restrictions on array size - must be greater than zero - don't need to apply to the returned value. expression is duplicated, but that's what macros are for (and if this compiles, it won't be recomputed because a. it's a constant, and b. the first usage is within a sizeof). Thus:

#define assert_constant(X) (sizeof((char[(X)*0+1]){ 0 }) * 0 + (X))

For bonus points, we can use a very similar technique to implement a static_switch expression, by combining array sizes with C11's _Generic (this probably doesn't have many practical uses, but might replace some cases of nested ternaries, which aren't popular):

#define static_switch(VAL, ...) _Generic(&(char[(VAL) + 1]){0}, __VA_ARGS__)
#define static_case(N) char(*)[(N) + 1]

char * x = static_switch(3,
             static_case(0): "zero",
             static_case(1): "one",
             static_case(2): "two",
             default: "lots");
printf("result: '%s'\n", x); //result: 'lots'

(We take the address of the array to produce an explicit pointer-to-array type, rather than let the implementation decide whether _Generic promotes arrays to pointers or not; as of April 2016 this ambiguity was fixed in the language by DR 481 and its ensuing TC.)

This is slightly more restrictive than assert_constant because it won't admit negative values. By putting a +1 in both the controlling expression and all of the case values, though, we can at least let it accept zero.

Alex Celeste
  • 12,824
  • 10
  • 46
  • 89
  • 1
    I couldn't get this to work on my compiler (xcc, which is "gcc" like but for a specific vendor's embedded MCU). If I use `(X)*0` or `(X)-(X)` the compiler seems to be clever and not evaluate it. Instead I used: `#define ASSERT_CONSTANT(X) (sizeof((char[((int)(X)&1)+1]){ 0 })*0 + (X))` which will work for floats and ints. – eresonance Jul 16 '20 at 18:10