C/C++ Compile Time Asserts
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
- typedef static_assert_test< sizeof(STATIC_ASSERT_FAILURE<false>) > _static_assert_typedef_
- typedef static_assert_test< sizeof(incomplete type) > _static_assert_typedef_
Similarly for true the type is not incomplete and the expansion is
- typedef static_assert_test< sizeof(STATIC_ASSERT_FAILURE<true>) > _static_assert_typedef_
- typedef static_assert_test< sizeof(valid type with one enum member) > _static_assert_typedef_
- 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
- 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
- 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...