Sunday, 13 January 2008

Why is Boost.StaticAssert so complex?

Boost.StaticAssert is a way to perform some assertions at compile time.

Perhaps you require longs to be twice as long as ints. You would write:


BOOST_STATIC_ASSERT(sizeof(long)==2*sizeof(int));

Now if you ever compile on a platform/compiler where this statement is false, the compilation will fail. Traditionally, I believe this was implemented as follows:

#define MY_STATIC_ASSERT(expr) int A[int(expr)];

Since arrays of size 0 are not allowed in C++, you would get an error like:

/tmp/test.cc:11: error: ISO C++ forbids zero-size array ‘A’

Which tells you nothing at all. If you saw this error you would have no idea why it occurred. until you navigated to the erroneous line (a non-issue with IDEs, for sure.) As a side-effect, if the assertion is true, it creates an array of size 1. Not very nice. So we need some way to ensure that the compilation can atleast show what happened without navigating to the source.

The latest version of boost/static_assert.hpp uses the fact that sizeof(T) is pretty much guaranteed to show T in the error message when T is an incomplete type (STATIC_ASSERTION_FAILURE<false> in the case of Boost.) The header file is full of if-defery that makes my eyes bleed. But it gets the job done:

/tmp/test.cc:9: error: invalid application of ‘sizeof’ to incomplete type ‘boost::STATIC_ASSERTION_FAILURE<false>’


I have a simple alternative. I've only tried it on Visual C++ and GNU G++ but I can't imagine why it wouldn't work on other compilers. Instead of depending on sizeof(incomplete-type) to show incomplete-type in the compile error, it references a nested type that doesn't exist:

#define STATIC_ASSERT(expr) typedef static_assert<(bool)(expr)>::STATIC_ASSERTION_FAILED static_assertion_t_12312;

template <bool x> struct static_assert;
template <> struct static_assert<true>{struct STATIC_ASSERTION_FAILED{};};
template <> struct static_assert<false>{};

This also gets the job done:

/tmp/test.cc:9: error: ‘STATIC_ASSERTION_FAILED’ in class ‘static_assert’ does not name a type

No code, no data and performs the same job in 4 lines of code. I must be missing something.

5 comments:

Leslie P. Polzer said...

Hmm... why not just use #error?

Sohail Somani said...

One reason is that you can't use #error within a pre-processor directive. So you would end up having to do:

#if sizeof(int)*2==sizeof(long)

#error long is not twice as big as an int!

#endif

Which is a lot more verbose than:

STATIC_ASSERT(sizeof(int)*2==sizeof(long));

Another reason is that the assertion may be used to validate a template instantiation:

template<typename T>
void foo()
{
STATIC_ASSERT(sizeof(T)==sizeof(int));
}

You could not do that with an ifdef/endif pair.

Anonymous said...

Another problem is that the expressive power (interpreted language features) of the preprocessor comes nowhere near the full power of the compiler. If you use the preprocessor's #if directive, your conditions are restricted. If you use the techniques shown above instead, you can formulate the (static) condition whatever C++ way you like. The two kinds of constraints are evaluated in different translation phases.

Anonymous said...

Umm... have you even looked at the code in static assert? It is basically doing what you are doing, along with a bunch of ifdef's to handle various idiosyncrasies of various compilers (and adding some nice touches like line numbers).

Some of those idiosyncrasies include cases where the compiler might be a bit lazy evaluation/instantiating a template, sometimes they produce warnings instead of errors unless you do extra work, etc.

Sohail Somani said...

@anonymous-2: I just parsed it this morning. It seems that most of the ifdef hacks are due to compilers not being able to handle the expressions as template parameters which the above would need to do as well. Oh well. Fortunately I don't need to handle MIPSpro, yet ;-)