compile time assertions in C

C has a facility for checking dynamic assertions at run-time. It's inside <assert.h> and its called assert. Now assert is a macro, so why isn't it called ASSERT? I don't know. Prior art no doubt. Anyway, assert is a dynamic runtime feature, you can only use it inside functions.

/* not inside a function, won't compile :-( */
assert(sizeof(int) * CHAR_BIT >= 32);  

That's a pity because it would be nice if I could get the compiler to check things like this automatically at compile time. I've occasionally seen an attempt at a compile-time check like this...

#if sizeof(int) * CHAR_BIT < 32
#error People of Earth. Your attention please...

But this doesn't work. The C preprocessor is a glorified text reformatter: it knows practically nothing about C. However, there is a way to write this as a compile time assertion (and moving any error trap to an earlier phase is a Good Thing)

{ yourself }

#define COMPILE_TIME_ASSERT(expr)   \
    char constraint[expr]

COMPILE_TIME_ASSERT(sizeof(int) * CHAR_BIT >= 32);

What's is going on here? Well, the example preprocesses to...

char constraint[sizeof(int) * CHAR_BIT >= 32];

If the expression is true, (an int is at least 32 bits), the expression will have a value of one, and constraint will be an array of one char. If the assertion is false, (an int is less than 32 bits), the expression will have a value of zero, and constraint will be an empty array. That's illegal, and you'll get a compile time error. Viola, a compile time assertion :-) You can use it inside and outside a function but you can't use it twice in the same function, as you end up with a duplicate definition. To solve that problem you could resort to some convoluted macro trickery:

#define COMPILE_TIME_ASSERT(expr)       char UNIQUE_NAME[expr]
#define UNIQUE_NAME                     MAKE_NAME(__LINE__)
#define MAKE_NAME(line)                 MAKE_NAME2(line)
#define MAKE_NAME2(line)                constraint_ ## line

But this is pretty horrible. Also, you will probably get warnings about unused variables. Take a step back for a moment and think about why it works at all. It's because you have to specify the size of an array as a compile time constant. The formal grammar of a direct-declarator tells you this. Let's look at some bits of grammar more closely:

Constrained arrays

  ( declarator )
  direct-declarator [ constant-expression opt ]
  direct-declarator ( parameter-type-list )
  direct-declarator ( identifier-list opt )

I just piggy backed on this, using the constraint that the value of the constant expression cannot (in this context) be zero. A natural question (to the curious) is are there other parts of the formal grammar that require a constant expression. The answer, of course, is yes.

Constrained enums

  enumeration-constant = constant-expression

However, I can't use this because there are no useful constraints in this context.

Constrained bit-fields

  declarator opt : constant-expression

Reading the constraints of a bit field I see that if the width of a bit-field is zero the declaration cannot have a declarator. In other words this is legal...

 struct x { unsigned int : 0; };

but this is not...

 struct x { unsigned int bf : 0; };

This suggests another way to create a compile time assertion

#define COMPILE_TIME_ASSERT(expr)   \
    struct x { unsigned int bf : expr; }

COMPILE_TIME_ASSERT(sizeof(int) * CHAR_BIT >= 32);

Trying this we again get duplicate definitions, not of a variable this time, but of the type struct x. However we can fix this by creating an anonymous struct:

#define COMPILE_TIME_ASSERT(expr)   \
    struct { unsigned int bf : expr; }

This works. However, now you'll probably get warnings about the unused untagged struct.

There is one last bit of grammar that uses a constant-expression.

Constrained switch

  identifier : statement
  case constant-expression : statement
  default : statement

It's well known that you can't have two case labels with the same constant. The following will not compile...

switch (0)
case 0:
case 0:;
So, here's yet another way to create a compile time assertion. This time we don't create a dummy variable, or a dummy type, but a dummy statement. A dummy switch statement:
#define COMPILE_TIME_ASSERT(pred)       \
    switch(0){case 0:case pred:;}

COMPILE_TIME_ASSERT(sizeof(int) * CHAR_BIT >= 32);

If pred evaluates to true (i.e., 1) then the case labels will be 0 and 1. Different; Ok. If pred evaluates to false (i.e., 0) then the case labels will be 0 and 0. The same; Compile time error. Viola. However, a switch statement cannot exist in the global scope. So the last piece of the puzzle is to put the compile time assertions inside a function.

#include <limits.h>

#define COMPILE_TIME_ASSERT(pred)            \  
    switch(0){case 0:case pred:;}

#define ASSERT_MIN_BITSIZE(type, size)       \
    COMPILE_TIME_ASSERT(sizeof(type) * CHAR_BIT >= size)

#define ASSERT_EXACT_BITSIZE(type, size)     \
    COMPILE_TIME_ASSERT(sizeof(type) * CHAR_BIT == size)

void compile_time_assertions(void)
    ASSERT_MIN_BITSIZE(char,  8);
    ASSERT_MIN_BITSIZE(int,  16);

No comments:

Post a Comment