Static Assertions in C

In the previous lesson we studied the concept of assertion, that is, a condition that a program must always satisfy during its execution. An assertion is completely different from an error which, instead, represents an anomalous condition that the program may encounter during its execution.

We have seen that the C language provides the assert() macro to define assertions that are verified during program execution. However, such assertions have two main defects:

  1. They slow down program execution;
  2. In case of error, the assertion is detected only during execution, abruptly interrupting the program.

Starting from the C11 standard, the C language provides a mechanism to define assertions that are verified during the program's compilation phase. These assertions are called static assertions. Obviously, not all conditions can be verified at compile time, but only those that depend on values known at that phase.

Let's see, in this lesson, how to define static assertions and how they work.

Key Takeaways
  • Static assertions allow verification of conditions during the program's compilation phase.
  • Static assertions are defined using the static_assert() macro, defined in the <assert.h> header.
  • Static assertions do not slow down program execution and allow detection of errors at compile time.
  • Static assertions can only verify conditions that depend on values known at compile time.

Defining static assertions

A static assertion in C language is defined using the static_assert() macro, defined in the <assert.h> header.

The syntax of the static_assert() macro is as follows:

static_assert(condition, message);

It works similarly to the assert() macro. In fact, if the value of the condition, which must be scalar, is false (that is 0), then the compiler generates a compilation error, showing the message provided as the second argument and will not generate the executable file. If, instead, the condition is true (that is different from 0), then the program is compiled normally.

A static assertion can be used to validate assumptions or hypotheses made by the programmer at compile time. Therefore, hypotheses regarding not the dynamic aspect of the program, but its static aspect, such as the size of a data type or the presence of a macro.

Examples of static assertions

Let's start with a first example.

Suppose we are creating a program that works with integers and the programmer wants to be sure that the int type is represented with at least 4 bytes. To do this, they can use the static_assert() macro as follows:

#include <assert.h>

/* ... */

static_assert(sizeof(int) >= 4,
              "The int type must be at least 4 bytes");

/* ... */

In this example, the static assertion verifies that the size of the int type is at least 4 bytes. Note that the sizeof operator can be used at compile time, since it returns a value known at that phase.

Unlike a dynamic assertion made with the assert() macro, a static assertion is a declaration in all respects, which is why it can appear at file level (that is, outside any function) or at block level (that is, inside a function or a code block delimited by braces {}).

Returning to the previous example, for instance, we could have created a source file as follows:

#include <assert.h>

static_assert(sizeof(int) >= 4, "The int type must be at least 4 bytes");

int main() {
    /* ... */
    return 0;
}

The static_assert() macro appears outside the main() function, but this is perfectly valid.

Let's see another application example. Suppose we want to create a function, called clear_stdin(), that empties the standard input buffer (stdin). It must read all characters present in the buffer, using the getchar() function, until it encounters a newline character ('\n') or the end of file (EOF).

The problem is that the getchar() function returns a value of type int, this is because it must be able to represent all possible characters, plus the special value EOF. However, the C language standard allows the int type to have the same range of values as the char type, which could cause problems in interpreting the EOF value. Therefore, in some implementations of the C language, the int type might not be able to represent the EOF value correctly.

We want to add, therefore, a static assertion that verifies that the int type is able to represent the EOF value. To do this, we can use the static_assert() macro as follows:

#include <assert.h>
#include <stdio.h>
#include <limits.h>

void clear_stdin() {
    int c;

    do {
        c = getchar();
        static_assert(UCHAR_MAX < INT_MAX,
                      "The int type must be able to"
                      "represent EOF");
    } while (c != '\n' && c != EOF);
}

In this example, the static assertion verifies that the maximum value representable by the unsigned char type (defined by the UCHAR_MAX macro in the <limits.h> header) is less than the maximum value representable by the int type (defined by the INT_MAX macro). We will study the <limits.h> header in a later lesson.

Note that the static assertion was inserted inside the clear_stdin() function, but this is perfectly valid. We acted this way so that the assertion is located near the code that depends on the truth of the assumption. This way, if the assertion is violated, we can directly modify the corresponding code.

Hint

Placement of static assertions

Although static assertions can be placed anywhere, it is always advisable to put them near the code that depends on the truth of the assumption. This way, if the assertion is violated, it is easier to identify the code that needs to be modified.

Let's see one last example.

Suppose we have a portion of program that copies a prefix string to another static string. We can use static_assert to verify, at compile time, that the destination string is large enough to contain the prefix string plus the null termination character ('\0').

#include <assert.h>

static const char prefix[] = "Message: ";
#define DESTINATION_SIZE 50
char destination[DESTINATION_SIZE];

/* ... */

static_assert(sizeof(destination) > sizeof(prefix),
              "The destination string must be"
              "large enough to contain"
              "the prefix");

strcpy(destination, prefix);

This way, if by mistake or distraction the programmer defines DESTINATION_SIZE with a value that is too small, the compiler will generate a compilation error, avoiding potential buffer overflow problems at runtime.

Advantages and Limits of Static Assertions

Static assertions, compared to normal or dynamic assertions, offer several advantages:

  1. First, they do not slow down program execution, since they are verified during the compilation phase.
  2. Second, they allow detection of errors at compile time, preventing the program from being executed in an invalid state.

Furthermore, there is no need to use precompilation directives to enable or disable static assertions, as happens for dynamic assertions with the NDEBUG macro.

However, static assertions have one important limitation: they can only verify conditions that depend on values known at compile time. Therefore, it is not possible to use static assertions to verify conditions that depend on dynamic data, such as user input or the state of a variable during program execution.

For example, if we wanted to verify that a certain variable x is positive during program execution, we could not use a static assertion, since the value of x is not known at compile time.

In fact, the following code would produce a compilation error:

#include <assert.h>

int x;

/* ... */

static_assert(x > 0, "The variable x must be positive");

If we try to compile this code, the compiler will generate an error, since x is a variable whose value is not known at compile time.

Conversely, if we had declared the variable x as const, then the compiler could know its value at compile time, allowing us to use a static assertion:

#include <assert.h>

const int x = 10;

/* ... */

static_assert(x > 0, "The variable x must be positive");

In general, the expression passed as the first argument to the static_assert() macro must be evaluable at compile time, otherwise the compiler will generate an error. It must not depend, that is, on variables or data that are not until the program is executed.