Partilhar via


C/C++ Compile Time Asserts

Noodles

The Problem

Run time asserts are fairly commonly used in C++. As the MSDN documentation for assert states

" (assert) Evaluates an expression and, when the result is false, prints a diagnostic message and aborts the program. "

There is another type of asserts which can be used to catch code issues right at the time of compilation. These are called static or compile-time asserts. These asserts can be used to do compile time validations and are very effectively used in the .NET Compact Framework code base.

E.g. you have two types Foo and Bar and your code assumes (may be for a reinterpret_cast) that they are of the same size. Now being in separate places there is always a possibility that someone modifies one without changing the other and that results in some weird bugs. How do you express this assumption in code? Obviously you can do a run-time check like

 assert(sizeof(foo) == sizeof(bar));

If that code is not hit during running this assert will not get fired. This might be caught in testing later. However, if you notice carefully all of the information is available during compilation (both the type and the sizeof is resolved while compilation). So we should be able to do compile time validation, with-something to the effect

 COMPILE_ASSERT(sizeof(int) == sizeof(char));

This should be tested during compilation and hence whether the code is run or not the assert should fail.

The Solution

There are many ways to get this done. I will discuss two quick ways

Array creation

You can create a MACRO expression as follows

 #define COMPILE_ASSERT(x) extern int __dummy[(int)x]

This macro works as follows

 // compiles fine 
COMPILE_ASSERT(sizeof(int) == sizeof(unsigned)); 
// error C2466: cannot allocate an array of constant size 0
COMPILE_ASSERT(sizeof(int) == sizeof(char)); 

The first expression gets expanded to int __dummy[1] and compiles fine, but the later expands to int __dummy[0] and fails to compile.

The advantage of this approach is that it works for both C and C++, however, the failure message is very confusing and doesn't indicate what the failure is for. It is left to the developer to visit the line of compilation failure to see that it's a COMPILE_ASSERT.

sizeof on incomplete type

This approach works using explicit-specialization of template types and the fact that sizeof of incomplete types fail to compile.

Consider the following

 namespace static_assert
{
    template <bool> struct STATIC_ASSERT_FAILURE;
    template <> struct STATIC_ASSERT_FAILURE<true> { enum { value = 1 }; };
}

Here we defined the generic type STATIC_ASSERT_FAILURE. As you see the type is incomplete (no member definition). However we do provide a explicit-specialization of that type for the value true. However, the same for false is not provided. This means that sizeof(STATIC_ASSERT_FAILURE<true>) is valid but sizeof(STATIC_ASSERT_FAILURE<false>) is not. This can be used to create a compile time assert as follows

 namespace static_assert
{
    template <bool> struct STATIC_ASSERT_FAILURE;
    template <> struct STATIC_ASSERT_FAILURE<true> { enum { value = 1 }; };

    template<int x> struct static_assert_test{};
}

#define COMPILE_ASSERT(x) \
    typedef ::static_assert::static_assert_test<\
        sizeof(::static_assert::STATIC_ASSERT_FAILURE< (bool)( x ) >)>\
            _static_assert_typedef_

Here the error we get is as follows

 // compiles fine
COMPILE_ASSERT(sizeof(int) == sizeof(unsigned)); 
// error C2027: use of undefined type 'static_assert::STATIC_ASSERT_FAILURE<__formal>
COMPILE_ASSERT(sizeof(int) == sizeof(char)); 

So the advantage is that the STATIC_ASSERT_FAILURE is called out right at the point of failure and is more obvious to figure out

The macro expansion is as follows

  1. typedef static_assert_test< sizeof(STATIC_ASSERT_FAILURE<false>) >  _static_assert_typedef_
  2. typedef static_assert_test< sizeof(incomplete type) >  _static_assert_typedef_

Similarly for true the type is not incomplete and the expansion is

  1. typedef static_assert_test< sizeof(STATIC_ASSERT_FAILURE<true>) >  _static_assert_typedef_
  2. typedef static_assert_test< sizeof(valid type with one enum member) >  _static_assert_typedef_
  3. typedef static_assert_test< 1 > _static_assert_typedef_

Put it all together

All together the following source gives a good working point to create static or compile time assert that works for both C and C++

 #ifdef __cplusplus

#define JOIN( X, Y ) JOIN2(X,Y)
#define JOIN2( X, Y ) X##Y

namespace static_assert
{
    template <bool> struct STATIC_ASSERT_FAILURE;
    template <> struct STATIC_ASSERT_FAILURE<true> { enum { value = 1 }; };

    template<int x> struct static_assert_test{};
}

#define COMPILE_ASSERT(x) \
    typedef ::static_assert::static_assert_test<\
        sizeof(::static_assert::STATIC_ASSERT_FAILURE< (bool)( x ) >)>\
            JOIN(_static_assert_typedef, __LINE__)

#else // __cplusplus

#define COMPILE_ASSERT(x) extern int __dummy[(int)x]

#endif // __cplusplus

#define VERIFY_EXPLICIT_CAST(from, to) COMPILE_ASSERT(sizeof(from) == sizeof(to)) 

#endif // _COMPILE_ASSERT_H_

The only extra part is the JOIN macros. They just ensure that the typedef is using new names each time and doesn't give type already exists errors.

More Reading

  1. Boost static_assert has an even better implementation that takes cares of much more scenarios and compiler quirks. Head over to the source at https://www.boost.org/doc/libs/1_36_0/boost/static_assert.hpp
  2. Modern C++ Design by the famed Andrei Alexandrescu

Comments

  • Anonymous
    October 26, 2008
    The biggest advantage to this technique is that it will help catch bugs when building release builds as well as debug builds. This makes me think that use of the standard preprocessor-time "assert" macro should output a warning at compile time, "Are you sure you can't make this a compile-time assert instead?"

  • Anonymous
    October 26, 2008
    Next C++ specification C++0x will have compile time assert built in. So maybe then we can think about this....

  • Anonymous
    October 26, 2008
    > #define COMPILE_ASSERT(x) extern int __dummy[(int)x] Careful here. This will also fail for all x<0, which are treated as "true" in C/C++ normally.

  • Anonymous
    November 10, 2010
    the answer to that last comment is COMPILE_ASSERT( x != 0 )

  • Anonymous
    November 10, 2010
    sorry, I wrote an error myself there. What I meant was: #define COMPILE_ASSERT(x) extern int __dummy[ (int) x!=0 ] btw the failing is the good part actually, imagine passing a big number to that macro, ahem... might turn your compile time check into a run time memory hog...