Post

Classic C++: Leveraging Compile-Time Checks with Static Assertions

If we develop embedded software, we must check static attributes as soon as possible to ensure robust and reliable code. Such checks could be expectations about the size of a pointer or user-defined type. A valuable mechanism to perform such checks at compile-time are static assertions.

Static Assertion
A mechanism in C++, also known as a compile-time assertion or compile-time check, that enables the verification of certain conditions at compile time.

Static Assertions give us a lot of benefits for our code:

  1. Compile-Time Error Detection: We can check static expectations already during the compilation process. That avoids runtime checks or crashes and can ensure correct behavior.
  2. Ensuring Type Safety: Static Assertions can enforce type safety by verifying assumptions about types, sizes, or relationships at compile time. So, for example, we can check if a user-defined type has padding.
  3. Portability and Cross-Platform Compatibility: Compile-time checks are handy when we want to port code to another platform. We can use them to ensure specific assumptions about our platform, like the size of data types.
  4. Documentation and Readability: Static Assertions can be a form of self-documentation. We can use them to define our assumptions and contracts or constraints in the source code, which improves the maintainability of our code and prevents misuse. By including meaningful messages in our assertions, we can also document the reason for our constrain. Additionally, we can improve the compiler output for template code by using static assertions to check type constraints beforehand.

C++11 introduces the keyword static_assert to trigger a compiler error if the condition check fails. To provide similar functionality for older C++ versions, we must enforce a compile error based on a condition evaluated at compile time. There are different ways to implement such checks. Let’s start with an implementation in C:

Static Assertions with C

Since C11 C also includes static assertion in the language with the keyword _Static_assert1. Before this inclusion, you could implement a compile-time assertion with macros like this2:

1
2
3
#define STATIC_ASSERT(cond, msg) typedef char msg[(cond) ? 0 : -1]

STATIC_ASSERT(sizeof(int) == 4, int_size_check);

This implementation defines a STATIC_ASSERT macro, which takes a condition (cond) and an indicator for the error message (msg). Inside the macro, a typedef is created. This typedef declares an array with msg as its name and a size of 0 or -1, depending on the condition cond. If cond is evaluated to true, the array will get a valid size of 0. However, if the condition is false, the array receives a negative length, which is invalid and will generate a compilation error like this:

1
2
<source>:1:58: error: size '-1' of array 'long_size_check' is negative
    1 | #define STATIC_ASSERT(cond, msg) typedef char msg[(cond) ? 0 : -1]

The array name in the error message provides you the information about the kind of error. In this case, the error is long_size_check, which tells us that the expected size for long does not match. The idea to utilize the array size for this kind of check was already described in 1997 in the article Compile-Time Assertions in C++ by Kevin S. Van Horn.

Static Assertions with C++

In C++, we can utilize partial template spezialisation to archive the same result. The following listing shows a possible implementation3:

1
2
3
4
5
6
7
8
9
10
template <bool CONDITION>
struct static_assertion;

template <>
struct static_assertion<true> {
    static_assertion() {}
    
    template<typename T>
    static_assertion(T) {}
};

The idea is simple: we declare our static_assertion in dependency of a boolean template parameter and then specialize it to allow the compiling if the condition is true. To use it, we need to instantiate an object of static_assertion:

1
const static_assertion<sizeof(int) == 4> ASSERT1("invalid size of int");

This object instantiation is quite lengthy but also provides a useful error message like:

1
2
<source>:13:50: error: variable 'const static_assertion<false> ASSERT2' has initializer but incomplete type
   13 | const static_assertion<sizeof(long) == 4> ASSERT2("invalid size of long");

However, it is possible to wrap the functionality into a macro to make it easier. Loki, for example, provides an implementation like this:

1
2
3
4
5
6
7
#define CONCAT( X, Y ) CONCAT_SUB( X, Y )
#define CONCAT_SUB( X, Y ) X##Y

#define STATIC_ASSERT(expr, msg) \
  enum { CONCAT(ERROR_##msg, __LINE__) = sizeof(static_assertion<expr != 0 >) }

STATIC_ASSERT((sizeof(int) == 4), invalid_size_of_int);

Summary

Static Assertions are a powerful tool to verify conditions and assumptions at compile time. Despite lacking the static_assert keyword, we can leverage Static Assertions in Classic C++ using templates or macros. Verifying conditions at compile time improves code reliability, catches errors early, and documents essential constraints. It helps us avoid runtime errors and port our code to other platforms or verify the code if we use other compilers.

References

Articles

Books

  • Andrei Alexandrescu: Modern C++ Design - Generic Programming and Design Pattern Applied (2000)
  • Davide Di Gennaro: Advanced Metaprogramming in Classic C++ (2015)

Libraries

  • Loki provides this functionality as CompileTimeError
  • Boost.StaticAssert provides the macros BOOST_STATIC_ASSERT and BOOST_STATIC_ASSERT_MSG

Footnotes

  1. with C23 _Static_assert will be renamed to static_assert 

  2. you can find the implementation in compiler explorer 

  3. you can find the implementation in compiler explorer 

This post is licensed under CC BY 4.0 by the author.