I l@ve RuBoard Previous Section Next Section

2.1 Compile-Time Assertions

As generic programming in C++ took off, the need for better static checking (and better, more customizable error messages) emerged.

Suppose, for instance, you are developing a function for safe casting. You want to cast from one type to another, while making sure that information is preserved; larger types must not be cast to smaller types.



template <class To, class From>


To safe_reinterpret_cast(From from)


{


   assert(sizeof(From) <= sizeof(To));


   return reinterpret_cast<To>(from);


}


You call this function using the same syntax as the native C++ casts:



int i = ...;


char* p = safe_reinterpret_cast<char*>(i);


You specify the To template argument explicitly, and the compiler deduces the From template argument from i's type. By asserting on the size comparison, you ensure that the destination type can hold all the bits of the source type. This way, the code above yields either an allegedly correct cast[1] or an assertion at runtime.

[1] On most machines, that is—with reinterpret_cast, you can never be sure.

Obviously, it would be more desirable to detect such an error during compilation. For one thing, the cast might be on a seldom-executed branch of your program. As you port the application to a new compiler or platform, you cannot remember every potential nonportable part and might leave the bug dormant until it crashes the program in front of your customer.

There is hope; the expression being evaluated is a compile-time constant, which means that you can have the compiler, instead of runtime code, check it. The idea is to pass the compiler a language construct that is legal for a nonzero expression and illegal for an expression that evaluates to zero. This way, if you pass an expression that evaluates to zero, the compiler will signal a compile-time error.

The simplest solution to compile-time assertions (Van Horn 1997), and one that works in C as well as in C++, relies on the fact that a zero-length array is illegal.



#define STATIC_CHECK(expr) { char unnamed[(expr) ? 1 : 0]; }


Now if you write



template <class To, class From>


To safe_reinterpret_cast(From from)


{


   STATIC_CHECK(sizeof(From) <= sizeof(To));


   return reinterpret_cast<To>(from);


}


...


void* somePointer = ...;


char c = safe_reinterpret_cast<char>(somePointer);


and if on your system pointers are larger than characters, the compiler complains that you are trying to create an array of length zero.

The problem with this approach is that the error message you receive is not terribly informative. "Cannot create array of size zero" does not suggest "Type char is too narrow to hold a pointer." It is very hard to provide customized error messages portably. Error messages have no rules that they must obey; it's all up to the compiler. For instance, if the error refers to an undefined variable, the name of that variable does not necessarily appear in the error message.

A better solution is to rely on a template with an informative name; with luck, the compiler will mention the name of that template in the error message.



template<bool> struct CompileTimeError;


template<> struct CompileTimeError<true> {};





#define STATIC_CHECK(expr) \


   (CompileTimeError<(expr) != 0>())


CompileTimeError is a template taking a nontype parameter (a Boolean constant). Compile-TimeError is defined only for the true value of the Boolean constant. If you try to instantiate CompileTimeError<false>, the compiler utters a message such as "Undefined specialization CompileTimeError<false>." This message is a slightly better hint that the error is intentional and not a compiler or a program bug.

Of course, there's a lot of room for improvement. What about customizing that error message? An idea is to pass an additional parameter to STATIC_CHECK and some how make that parameter appear in the error message. The disadvantage that remains is that the custom error message you pass must be a legal C++ identifier (no spaces, cannot start with a digit, and so on). This line of thought leads to an improved CompileTimeError, as shown in the following code. Actually, the name CompileTimeError is no longer suggestive in the new context; as you'll see in a minute, CompileTimeChecker makes more sense.



template<bool> struct CompileTimeChecker


{


   CompileTimeChecker(...);


};


template<> struct CompileTimeChecker<false> { };


#define STATIC_CHECK(expr, msg) \


   {\


       class ERROR_##msg {}; \


       (void)sizeof(CompileTimeChecker<(expr) != 0>((ERROR_##msg())));\


   }


Assume that sizeof(char) < sizeof(void*). (The standard does not guarantee that this is necessarily true.) Let's see what happens when you write the following:



template <class To, class From>


To safe_reinterpret_cast(From from)


{


   STATIC_CHECK(sizeof(From) <= sizeof(To),


      Destination_Type_Too_Narrow);


   return reinterpret_cast<To>(from);


}


...


void* somePointer = ...;


char c = safe_reinterpret_cast<char>(somePointer);


After macro preprocessing, the code of safe_reinterpret_cast expands to the following:



template <class To, class From>


To safe_reinterpret_cast(From from)


{


   {


      class ERROR_Destination_Type_Too_Narrow {};


      (void)sizeof(


         CompileTimeChecker<(sizeof(From) <= sizeof(To))>(


            ERROR_Destination_Type_Too_Narrow()));


   }


   return reinterpret_cast<To>(from);


}


The code defines a local class called ERROR_Destination_Type_Too_Narrow that has an empty body. Then, it creates a temporary value of type CompileTimeChecker< (sizeof(From) <= sizeof(To))>, initialized with a temporary value of type ERROR_ Destination_Type_Too_Narrow. Finally, sizeof gauges the size of the resulting temporary variable.

Now here's the trick. The CompileTimeChecker<true> specialization has a constructor that accepts anything; it's an ellipsis function. This means that if the compile-time expression checked evaluates to true, the resulting program is valid. If the comparison between sizes evaluates to false, a compile-time error occurs: The compiler cannot find a conversion from an ERROR_Destination_Type_Too_Narrow to a CompileTimeChecker<false>. And the nicest thing of all is that a decent compiler outputs an error message such as "Error: Cannot convert ERROR_Destination_Type_Too_Narrow to CompileTimeChecker <false>."

Bingo!

    I l@ve RuBoard Previous Section Next Section