Features


Compile-Time Assertions in C++

Kevin S. Van Horn

You can get templates to do all sorts of things for you, even check assertions at compile time.


As a general rule, it's best to catch your programming errors as early as possible. The assert macro can help a lot here, by detecting violated assumptions before your code has a chance to act on those assumptions. assert works at run time, but the best time to catch an error is before your program runs — at compile time. If your assertion is a constant expression, then in principle the compiler should able to check it for you.

In practice, checking a constant expression at compile time is not entirely straightforward. The most obvious approach is to make use of the preprocessor:

#if !(Assertion)
#error You blew it!
#endif

This approach has some severe limitations. Since the preprocessor knows very little about C++, an #if directive can't use the sizeof operator or refer to enum values, class members, const variables, or integer-valued template parameters. The preprocessor is okay for checking requirements on #defined symbols, but that's about it.

A Motivating Example

Consider using a multiplicative congruential algorithm to generate pseudorandom numbers, using Schrage's algorithm to avoid integer overflow. This algorithm requires a pair of integers m and a satisfying the following restriction:

0 < a && a < m && a % m < m / a

If you've done much generic programming, you probably don't want to write just a particular random-number generator; you can write a random-number generator template:

template <unsigned m, unsigned a>
class randgen {
public:
  randgen(unsigned seed);
  unsigned operator()() const;
private:
 ...
};

To create a random-number generator you then write

randgen<M, A> x(seed);

using your favorite constant M and A, and thereafter calls x to generate a new random number. However, being an experienced programmer, you know that eventually someone is going to ignore your carefully-crafted warning comments and use randgen<M,A> with constants M and A that don't satisfy the restriction. It would be nice to associate some sort of compile-time check with the randgen<> class template itself. Clearly, the preprocessor will be no help at all with this.

The ctassert<> Class Template

The ctassert<> class template shown below can perform this compile-time check:

#ifndef CTASSERT_H
#define CTASSERT_H
     
template <bool t>
struct ctassert {
  enum { N = 1 - 2 * int(!t) };
    //  1 if t is true,
    // -1 if t is false
  static char A[N];
};
     
template <bool t>
char ctassert<t>::A[N];
     
#endif

Just insert the declaration

ctassert<
  0 < a && a < m && a % m < m / a
> foo;

someplace within the definition of of the randgen<> class template. If randgen<> is instantiated with invalid values for m and a, ctassert will force a compile-time error.

Here's how it works. When you declare a dummy variable of type ctassert<E>, where E is a constant, Boolean, or integer expression, the compiler generates the definition

static char A[1];

if E is true (non-zero), and

static char A[-1];

if E is false (zero). The latter is an error, so the compiler will complain loudly if E happens to be false. Furthermore, the compiler diagnostic will point to the declaration of the variable of type ctassert<E>, telling you exactly what you did wrong.

Note that an object of type ctassert<E> has no internal storage, so adding a data member of this type to an object does not increase that object's size.

One word of warning: when writing a compile-time assertion ctassert<E>, make sure that the expression E does not contain the > operator, or your compiler is likely to consider it a closing angle bracket. Rewrite the expression to use < instead.

A More Involved Example

The following example uses those nifty numeric_limits<> traits classes in the draft C++ Standard Library. It applies specifically to those situations in which a programmer knows exactly what size of integer — 8-, 16-, or 32-bit — to use. Unfortunately, the language definition provides no portable way of specifying a particular size of integer. The best the programmer can do is write a header file (call it sized_types.h) giving typedefs for int8, int16, int32, etc. These typedefs will be specific to the particular platform in use, so the programmer can only hope that someone remembers to change the typedefs when the code is moved to a new platform.

The C++ standardization committee has helped out here. The proposed Standard Library includes a class template numeric_limits<>, which is specialized for each of the numeric fundamental types. The class numeric_limits<T> includes the following members:

static const bool is_integer;
  // true if T is an integer type
static const bool is_signed;
  // true if T is a signed type
static const int radix;
  // for integer types, specifies
  // the base of the representation
static const int digits;
  // for built-in integer types,
  // the number of non-sign bits

Compile-time assertions can check that the typedefs in sized_types.h are correct. If the code is moved to another platform and the header contains the wrong typedefs, the compiler will not so gently inform the programmer.

Listing 1 shows how to write the compile-time checks. There are a lot of them; thus, they reside in their own namespace to avoid polluting the global namespace with dummy variable names. For each sized type the compile-time assertion checks that the type is in fact an integral type; that it is signed or unsigned as desired; that it uses a base-2 representation; and that it has the appropriate number of bits.

Assertions: they're not just for run time anymore! o

Kevin S. Van Horn has a Ph.D. in Computer Science from Brigham Young University. He was the technical lead for Excite, Inc.'s NewsTracker service until he decided to strike out on his own and start KSVH Software and Consulting (http://www.xmission.com/~ksvhsoft/). You can reach him at kevin.s.vanhorn@iname.com.