Channels ▼
RSS

Compile-Time Assertions & Debugging


December, 2005: Compile-Time Assertions & Debugging

Catch bugs earlier with assertions

Klaus Wittlich is a senior developer and in-house teacher for software technology at SAE IT-systems in Cologne, Germany. He can be reached at klaus_wittlich@sae-it.de.


A common technique for catching bugs at runtime is by using assertions, which check invariants at runtime. If an invariant is known at compile time, the compiler can check it and you know about the inconsistency after compiling. A typical application for this case is the exact number and order of elements of an array. You save time by not having to link and run the program to catch the error. Moreover, you can use compile-time assertions for some documentation purposes. Sometimes they are a better choice than switch-case statements, because they lower the risk of forgotten cases. All techniques described in this article have been succesfully used during a project developed with VC++ 2003.

Basic Compile-Time Assertions

One of the simplest compile-time assertions is based on using the #if/#endif couple. In this code, if someone changes M, the code will not compile:

#define M 5

#if M != 5
illegal statement
#endif

Another idea is based on illegal array ranges if a condition is not fulfilled [1]. The principle is:

int ai[COND ? 1 : -1];

where COND is a condition that is evaluated at compile time. If COND evaluates to true, the expression COND ? 1 : -1 evaluates to 1, otherwise to -1. -1 is an illegal array size, so the compiler will throw an error. If you replace -1 with 0, some compilers will only raise a warning. This way, you can tune your compile-time assertions, reporting only warnings or full errors. It might seem that this method wastes space. You will pay if you use that line of code in a function, but not if that line of code occurs in a structure that is never used. Keep in mind that you can define structures within functions. If an object of that type is never used, there will be no bloat. Instead of using the naked array in a function, you can enclose it in a local class that is never used:

class NeverUsed{ int ai[COND ? 1 : -1 ];};

If class NeverUsed is never used to instantiate an object, there will be no bloat caused by this code.

Another idea for compile-time assertions is to use the size of an incomplete defined type in case of an error and the size of a fully defined type otherwise [1]. You can easily use templates for this purpose—dependent on the template parameter, you have either a complete or incomplete type. For example:

template <bool> class OnlyTrue;
template <> class OnlyTrue<true> {};

OnlyTrue<true> is a type of known size and OnlyTrue<false> has unknown size, so sizeof(OnlyTrue<true>) can be evaluated while sizeof(OnlyTrue<false>) cannot. What about the costs if you use this within a function? A statement such as:

27;

is a legal statement and is optimized away by the compiler, so you have nothing to pay for it, either in execution time or memory. To be lazy, we make a macro:

#define CTA(X) sizeof(OnlyTrue<(bool)(X)>)

Please note that the macro is not terminated by a semicolon, so you can use it within structures if necessary, too. This can be useful in cases I will describe later in this article. The OnlyTrue template was designed by the following principle: Give an incomplete type definition of a general template and offer a complete type definition for those specializations you desire. You can also go for the opposite design philosophy—define a complete type of a generalized template and define incomplete specializations. For example:

template<int> class AvoidN {};
template <> class AvoidN<17>;

Here, AvoidN17<17> is forwarded. sizeof(AvoidN<N>); will not compile only if N == 17. You can easily add more numbers or enum values to be avoided. They may be shared over more than one header file—this is more flexible than checking all illegal numbers individually, like this:

sizeof(OnlyTrue<N!=17 || N != 25 || N 	!= 37 || ...>);

This becomes unreadable if you have a large set of forbidden numbers. Moreover, if you need this long or condition in more than one place, you should avoid code multiples.

One obvious use of compile-time assertions is to check versions of a library you include (or something else). You only need to provide a dummy function and test the version at compile time:

void foo() {
    CTA(VERSION == CURRENT_VERSION);
}

Sometimes compile-time assertions are a better choice than documentation. Here is an example from my own experience: Within a large software project, you have a lot of classes and a lot of functions that deal with them. For example, one displays your class on the screen, another reads your class from a database. If you add a new member to your class, you'll need to touch all functions that deal with your class. Compile-time assertions are helpful here to show you all functions to touch. You add a version to your classes and let the compiler check them:

struct MyClass {
    enum {VERSION = 1};
    ....
void foo(MyClass* p) {
    CTA(MyClass::VERSION == 1);

If you now add a new member to MyClass and change VERSION to 2, foo will not compile. But if you change the version request in foo and add code for your new member, a reader of your source code can now see that foo is adjusted to version 2 of your class and was not overlooked.

Checking Arrays

Compile-time assertions can check your hard-coded arrays for proper size and even for the proper order. You can often use these techniques to avoid switch/case constructs where cases may be easily forgotten.

Often, such constructs are related to enums. Here's a short example. You use a typical enum:

enum E { e0, e1, e2, e_Last}

e0, e1, and e2 are important for the modeling of your real problem, and e_Last is important for the compile-time assertions. If you add a new enum to your list, you must add it before e_Last. e_Last is the count of your enums. To count the elements of an array, we use:

#define ELEMENT_COUNT(X) (sizeof(X)/sizeof(X[0]))

Please note that X must not be a pointer here. You would get sizeof the pointer in the nominator, not sizeof what the pointer points to. Now we want a function that returns a text for each E. Instead of implementing a switch/case, you can use a hard-coded array:

const char * foo(E e) {
    static const char * ret[] = {
        "Text 0","Text 1","Text 2"
    };
    CTA(ELEMENT_COUNT(ret) == e_Last);
    return ret[e];
}

In the previous case shown, the world is small and easily comprehensible. But if you have large arrays, the probability for mistakes grows. You need a technique to find the places of insertions easily and to check for the right order. Instead of an array of const char * we use a struct that combines a check field and a char *. The first enum must be zero and we need a compile-time assertion to test that. The difference between successive enums must be 1. With the principles just shown, the following code should make sense:

template <int> struct IsZero;
template <> struct IsZero<0> {};
#define IS_ZERO(X) (sizeof(IsZero<(X)>))
template <int> struct IsOne;
template <> struct IsOne<1> {};
#define IS_ONE(X) (sizeof(IsOne<(X)>))

With that combination, we can write a much safer version of foo:

const char * foo(E e) {
    struct t_ret {size_t NotUsed; 
	 char * text;};
    static const t_ret ret[] = {
        {IS_ZERO(e0), "Text 0"},
        {IS_ONE(e1-e0), "Text 1"},
        {IS_ONE(e2-e1), "Text 2}
    } ;
    CTA(ELEMENT_COUNT(ret) == e_Last);
    return ret[e].text;
}

Be aware that there will be a cost in static memory for the compile-time assertions, so this technique might not be your best choice when memory is scarce.

Often you need to do something dependent on your enum values. I showed how to use structs with text. Instead of text you can use function pointers, or functor pointers instead of calls. Functors have the advantage that you can write them as local functions with different parameter sets. This way, you can get a compile-time checkable alternative choice to the switch/case statement.

Some Drawbacks

Not everything that is known at compile time can easily be checked in a useful way by a compile-time assertion. One example is hard-coded text. The following will not work in the desired way:

CTA("Text 1" == "Text 2");

Another drawback is the use of functions in a table instead of a switch statement. Functions have an associated call overhead that code in the switch statement does not have. Therefore, they are a little slower than a switch statement.

Example

For more example code that demonstrates the techniques described in this article, see file "SampleCode.cpp" in the code for this article at http://www.cuj.com/code/.

References

  1. Alexandrescu, Andrei. Modern C++ Design, Addison-Wesley, 2001.
CUJ


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video