C_ASSERT, the Compiler Assert

We want our code, in C or C++ in this case, to be as robust as possible. By “Robust” I mean resilient to bugs, changes and surprises. We can increase our code’s run-time robustness by employing various good coding practices, “assert” constructs or even just ‘if’ statements in the right places to protect against broken assumptions and other dire straights. But wouldn’t it be so much better if we can catch problems before even running a single line of code? The C_ASSERT macro will do just that.

C_ASSERT

The C_ASSERT macro is what you need in order to protect your code against many problems, at compilation time. It can defined like suggested here in the Microsoft knowledge base, or like the following which is better in my view since it avoids many caveats of unique usage and more:

#define C_ASSERT(phrase) extern char _C_ASSERT_[(phrase) ? 1 : -1]

This declares the existence of an external array, based on the evaluation of the given phrase. If the phrase evaluates as true, it will declare the external array _C_ASSERT_ to be a char array of size 1. Otherwise, it will declare it as a char array of the illegal size -1. This will fail the compilation.

Why define C_ASSERT like that?


This is definition makes sense since:

  • There is no “cost” here, no memory-consuming entity is being defined, just a non-binding declaration. You don’t really need to have such an array anywhere
  • The macro can be used in multiple locations without collisions
  • The macro can be used inside and outside of scopes

You can see a detailed discussion on how this macro can be defined in this great pixelbeat post, to which I give credit for the idea of using ‘extern’.

Build Time Robustness

With this little tool at hand, we can do a much better job at maintaining Build Time Robustness. We can use C_ASSERT to “bind” some of our defined constructs to each other’s limitations, to assert assumptions that must not break, or just express changeable assumptions with code instead of comments. Following are several examples.

Bit Field guarding

Let’s assume you have defined a small Bit Field (see more about Bit Fields in my post about Masks VS. bitfields), and you plan that field to express values that are defined by an enum. Your code may look like this:

typedef struct

{

       unsigned int color : 3;

       unsigned int shape : 1;

       unsigned int RSVD  : 28;

}OBJECT;

 

 

 

typedef enum

{

       COLOR_BLUE   = 0,

       COLOR_RED    = 1,

       COLOR_YELLOW = 2,

 

       COLOR_MAX // Leave that last!!

}COLORS;

 

 

typedef enum

{

       SHAPE_ROUND = 0,

       SHAPE_RECT  = 1,

 

       SHAPE_MAX // Leave that last!!

}SHAPES;

When you try to assign a large number to a small field directly (with immediate value), the compiler will stop you, which is good. However this is a “thin” protection mechanism. For example, you can assign a function argument that can contain large numbers into a small field, and it will pass compilation:

OBJECT * allocate_object(COLORS color, SHAPES shape)

{

       OBJECT * object = (OBJECT*)malloc(sizeof(OBJECT));

 

       // ‘color’ might contain a large number

       object->color = color;

       // ‘shape’ might contain a large number

       object->shape = shape;

 

       return object;

}

If the COLOR or SHAPE enums are ever enlarged, it can break this code that has a hidden dependency in it: the enums are defined with maximum values that are supposed to fit into the respective bit fields.

We can mitigate the problem by adding a C_ASSERT at the right place. Where is that? I personally put it in the using location, not at the definition of the problematic entity. So in this case, I would not put it at the definition of the enums, but at the definition of the OBJECT struct, in the function at danger, or in both. No need to use C_ASSERT sparingly,it is free! It has an effect only during the build, not during runtime. 

typedef struct

{

       unsigned int color : 3;

       unsigned int shape : 1;

       unsigned int RSVD  : 28;

}OBJECT;

C_ASSERT(COLOR_MAX <= 8);

C_ASSERT(SHAPE_MAX <= 2);

 

 

OBJECT * allocate_object(COLORS color, SHAPES shape)

{

       OBJECT * object = (OBJECT*)malloc(sizeof(OBJECT));

 

       C_ASSERT(COLOR_MAX <= 8);

       object->color = color;

       

       C_ASSERT(SHAPE_MAX <= 2);

       object->shape = shape;

 

       return object;

}

If I try to add the element SHAPE_TRIANGLE to SHAPES and compile, I get this:

Bingo!

Struct size fortification

Another example: Assume you have a struct comprised of other structs. For reasons of binary backwards compatibility, your struct must stay at a fixed size (say 3 DWORDS == 12 bytes). If anyone (you?) goes and changes one of the underlying struct definitions, it can break that restriction without anybody noticing. We want it to at least be noticed so the problem can be addressed. Here is how you can define such a struct, while using C_ASSERT to guard against un-noticed size changes:

typedef struct

{

       HEADER header;

       FLAGS  flags;

       DATA   data;

}PACKET;

C_ASSERT(sizeof(PACKET) == 12);

This “locks” the struct to exactly that size. It can be changed if needed, but it will save you from bad surprises.

Summary

This little macro can save you lots of tears, and the two examples above come from stories I had in real life. I will use that macro extensively in future posts about Build-Time robustness, stay tuned!

156 Comments

Leave a Comment

Your email address will not be published. Required fields are marked *