Precondition checking

Back to programming

Setting up the situation

We start from the situation that we have implemented a data structure to hold a dynamically sized 2-dimensional array. The data structure is implemented as a class template 'Array2' holding elements of type 'Type'. Such an array holds the data as well as its width and height. One particular member function 'setSize' is responsible for resizing the array whenever that is needed.

template <typename Type>
class Array2
{
public:
    // ...
    void setSize(integer width, integer height,
        const Type& defaultData);    
    // ...
private:
    Type* data_;
    integer width_;
    integer height_;
};

It is quite clear what the 'setSize' function should do whenever the passed-in parameters 'width' and 'height' are non-negative: it should resize the array to the desired dimensions, possibly padding the new elements, due to expansion, with 'defaultData'. However, negative 'width' or negative 'height' makes no sense. How should we react in such situations?

Reactions

One could argue that we should react in a forgiving way by clamping the negative value to zero, kind of like mapping an invalid value back to some valid value. However, doing so is a disservice to the users of the function. This is because such an invalid value reflects a bug at the callers side with a 100% certainty: either the caller has misunderstood the usage of the function, or the value is a consequence of some less intentional bug. An example of such a bug would be an incorrect formula used to compute the desired width. In particular, in programming it seems easy to get formulas off by one (giving -1 etc).

Another could argue that it is the caller's duty to check his/her variables before calling the function. However, this is clearly not practical. An n amount of calls induces n amount of precondition checks. Besides, if the author of the function changes the preconditions in a backwards compatible way, the precondition checks that the caller placed are suddenly wrong and there are n of those. However, if we place the precondition check in the beginning of the function, this single check works for all calls and is also thus updatable.

Because an invalid value always reflects a bug at the callers side, such an event should never go without a notification. But this in itself is still too weak a reaction. If we have detected the presence of a bug, then it is very probable that in the very near microseconds the program ends up doing things it wasn't supposed to do. The problem is that once the program gets in an inconsistent state, there is nothing you can do to get back to a consistent state. This is because even the destructors (used for roll-back) can be affected by the inconsistent state. The only thing you can do is to call 'abort()' which terminates the program brutally without executing any additional code (like destructors).

assert to the rescue?

We need a handy way to test for a condition, and if it is not satisfied, print the failed condition and involved variables, and then abort(). The C standard library (and thus C++) has a macro 'assert' that is almost a match for what we seek. It works like this:

#include <cassert>

// ...

template <typename Type>
void Array2<Type>::setSize(
    integer width, integer height,
    const Type& defaultData)
{
    assert(width >= 0);
    assert(height >= 0);
    // ...
}

The 'assert' is a macro that does many of the things we want. It uses the preprocessor to convert the string inside the parentheses to a text string (as in 'width >= 0') which can be printed along with the error message. In addition, by including the __FILE__ and __LINE__ preprocessor macros, the error message also includes the line and file from where the assertion was triggered from. This is one of the few cases where the use of preprocessor macros is justified. In addition, again by using the preprocessor, the code induced by the assert macro is not generated if the preprocessor NDEBUG is defined. This means that the checking in the release version costs nothing because it is not done.

The 'assert' is nice, but it has a few shortcomings. First, given that the condition 'width >= 0' does not hold, we would certainly be interested to see what the value of 'width' actually was. However, 'assert' does not provide you a way to do this. Second, it still could be that in a release version some conditions get failed in some not-yet-tested situations in which case terrible things get to happen again. However, the worst thing is that there will be no indication of the reason! We will address this second question first.

Performance

Ideally, of course, you would always check for the preconditions. However, the worry is in the performance. If you check for the preconditions in the release version also, will the performance get downgraded by too much an amount? The answer to this question depends on your function. If the function takes a lot of time, then the relative time of the precondition checks are something next to nothing. For example, the function 'setSize' in this article belongs to this class. Thus it makes sense to always check the preconditions for these kinds of functions. However, if the function is short and called often, the relative performance hit can be big, especially because modern computers choke on comparisons. As a prime example, consider indexing an std::vector. Then checking the index everytime you access an element induces a great performance hit. The same thing applies to indexing mathematical vectors and matrices. Because the performance degradation is such big, you will want to disable the checks for these short functions in the release version. On the positive side, off-indexing is easily revealed when testing a feature using it in debug mode. In debug versions the precondition checks should always be present, of course.

As a rule of thumb I use the following: if the function is of the order O(lg n) or less (or something to that direction), then I will want to disable the precondition checks in the release version. However, for functions of greater time usage, I always use precondition checks.

ENSURE, ASSERT and REPORT: assert's on steroids

To remedy the shortcomings of 'assert', we will now introduce a set of macros that matches our needs.

#define ENSURE(expr)\
{if (!(expr)) {Pastel::Detail::error(#expr, __FILE__, __LINE__);}}

#define ENSURE1(expr, a)\
{if (!(expr)) {Pastel::Detail::error(#expr, __FILE__, __LINE__, #a, (double)(a));}}

#define ENSURE2(expr, a, b)\
{if (!(expr)) {Pastel::Detail::error(#expr, __FILE__, __LINE__, #a, (double)(a), #b, (double)(b));}}

#define ENSURE3(expr, a, b, c)\
{if (!(expr)) {Pastel::Detail::error(#expr, __FILE__, __LINE__, #a, (double)(a), #b, (double)(b), #c, (double)(c));}}

We call the macros 'ENSURE' to differentiate from 'assert'. The number at the end reflects the number of variables that it can report. It's purpose is to transmit, in case of failed condition, the condition string, file name, line number, the names of the reported variables and their values to a function that does the error reporting and 'abort()'s. Using these macros, we check our 'setSize' function as follows:

// ...

template <typename Type>
void Array2<Type>::setSize(
    integer width, integer height,
    const Type& defaultData)
{
    ENSURE1(width >= 0, width);
    ENSURE1(height >= 0, height);

    // ...
}

Note that we have taken an approach in which the reported variables are assumed to be convertible to a floating point double. This is usually the case. But in the cases this does not hold, we fall back to ENSURE, which does not report variables, but does all the other things. You might choose another way (templates maybe). However, the idea should be clear by now. To handle those performance critical functions I use the following set of macros:

#if (PASTEL_ENABLE_PENSURES != 0)

#define PENSURE(expr) ENSURE(expr)
#define PENSURE1(expr, a) ENSURE1(expr, a)
#define PENSURE2(expr, a, b) ENSURE2(expr, a, b)
#define PENSURE3(expr, a, b, c) ENSURE3(expr, a, b, c)
#define PENSURE4(expr, a, b, c, d) ENSURE4(expr, a, b, c, d)

#else

#define PENSURE(expr)
#define PENSURE1(expr, a)
#define PENSURE2(expr, a, b)
#define PENSURE3(expr, a, b, c)
#define PENSURE4(expr, a, b, c, d)

#endif

The macros are called 'PENSURE' for 'Performance ENSURE'. They work in the same way as the 'ENSURE' macros, but are disabled whenever 'PASTEL_ENABLE_PENSURES' is not defined. In a similar fashion, I have defined a family of 'ASSERT' macros that are disabled in debug mode:

#if (PASTEL_DEBUG_MODE == 0)

#define ASSERT(expr)
#define ASSERT1(expr, a)
#define ASSERT2(expr, a, b)
#define ASSERT3(expr, a, b, c)
#define ASSERT4(expr, a, b, c, d)

#else

#define ASSERT(expr)\
{if (!(expr)) {Pastel::Detail::assertionError(#expr, __FILE__, __LINE__);}}

#define ASSERT1(expr, a)\
{if (!(expr)) {Pastel::Detail::assertionError(#expr, __FILE__, __LINE__, #a, (double)(a));}}

#define ASSERT2(expr, a, b)\
{if (!(expr)) {Pastel::Detail::assertionError(#expr, __FILE__, __LINE__, #a, (double)(a), #b, (double)(b));}}

#define ASSERT3(expr, a, b, c)\
{if (!(expr)) {Pastel::Detail::assertionError(#expr, __FILE__, __LINE__, #a, (double)(a), #b, (double)(b), #c, (double)(c));}}

#define ASSERT4(expr, a, b, c, d)\
{if (!(expr)) {Pastel::Detail::assertionError(#expr, __FILE__, __LINE__, #a, (double)(a), #b, (double)(b), #c, (double)(c), #d, (double)(d));}}

#endif

The 'ASSERT' macros are used inside of data structures and such to assert invariants, but are not used to check preconditions. They are a very handy tool to make sure everything is consistent inside the implementation. Finally, I will introduce the set of 'REPORT' macros which are just used to report if a condition is satisfied (note, the inverse of how 'ENSURE' and 'ASSERT' work), make the same printouts as 'ENSURE' and 'ASSERT', but not 'abort()'. Note the implementations are also different compared to 'ENSURE' and 'ASSERT'.

#define REPORT(expr)\
    ((expr) && (Pastel::Detail::report(#expr, __FILE__, __LINE__), true))

#define REPORT1(expr, a)\
    ((expr) && (Pastel::Detail::report(#expr, __FILE__, __LINE__, #a, (double)(a)), true))

#define REPORT2(expr, a, b)\
    ((expr) && (Pastel::Detail::report(#expr, __FILE__, __LINE__, #a, (double)(a), #b, (double)(b)), true))

#define REPORT3(expr, a, b, c)\
    ((expr) && (Pastel::Detail::report(#expr, __FILE__, __LINE__, #a, (double)(a), #b, (double)(b), #c, (double)(c)), true))

#define REPORT4(expr, a, b, c, d)\
    ((expr) && (Pastel::Detail::report(#expr, __FILE__, __LINE__, #a, (double)(a), #b, (double)(b), #c, (double)(c), #d, (double)(d)), true))

The intent of this kind of implementation is that it makes the following possible:

if (REPORT1(a >= 0, a))
{
    // Do something..
}

The reporting functions are as follows:

namespace Pastel
{

    namespace Detail
    {

        // Prints a report message. Used by REPORT macros.

        PASTELSYS void report(
            const char* testText = 0,
            const char* fileName = 0, int lineNumber = -1,
            const char* info1Name = 0, double info1 = 0,
            const char* info2Name = 0, double info2 = 0,
            const char* info3Name = 0, double info3 = 0,
            const char* info4Name = 0, double info4 = 0);

        // Prints an error message and aborts the program.

        PASTELSYS void error(
            const char* testText = 0,
            const char* fileName = 0, int lineNumber = -1,
            const char* info1Name = 0, double info1 = 0,
            const char* info2Name = 0, double info2 = 0,
            const char* info3Name = 0, double info3 = 0,
            const char* info4Name = 0, double info4 = 0);

        // Prints an error message and aborts the program.

        PASTELSYS void assertionError(
            const char* testText = 0,
            const char* fileName = 0, int lineNumber = -1,
            const char* info1Name = 0, double info1 = 0,
            const char* info2Name = 0, double info2 = 0,
            const char* info3Name = 0, double info3 = 0,
            const char* info4Name = 0, double info4 = 0);

    }

}

Summary

Precondition checks are a great debugging device, the intent being to detect as much of the inconsistencies induced by bugs as possible. The place for the precondition check is at the beginnning of the function. If a precondition check is violated, the program is certainly in an inconsistent state and the only safe option is to 'abort()'. In short functions the checks can induce a relatively big performance hit. In these functions you must, for practical performance reasons, disable the checks, but only in release versions. The C standard library 'assert' is a good start for precondition checks, but it has two shortcomings: you can't print out the involved variables and the checks are removed in release versions. These shortcomings can be fixed by implementing your own macros that fulfill the requirements.