mr-edd.co.uk :: horsing around with the C++ programming language

An improved assert macro for comparisons

[22nd September 2009]

Over the last few days I've been battling with some code that dances on the borders of floating point precision.

assert()s such as the following were getting hit occasionally:

assert(areaOfTriangle > 0);

But when this kind of assertion fails it doesn't tell you all you might like; it would be great to actually see the value of areaOfTriangle. If it's 0 or a tiny negative number, then perhaps the triangle is so small that the precision of the floating point type used may be exhausted. However, if areaOfTriangle is a big negative number, then it's likely you've made a more fundamental error somewhere else in the code. In other words, it doesn't tell you much about why the failure occurred.

Since I'm one of those crazy people that think debuggers are a sign of weakness, I thought about creating an alternative to the traditional assert() macro that would also print the values of the operands on failure.

Requirements

We'll create a macro called assert_relation() that is invoked as follows:

assert_relation(areaOfTriangle, >, 0); // like assert(areaOfTriangle > 0)

You could argue that the macro should be called ASSERT_RELATION() instead, but I think the fact that it looks rather like the original assert() and that the second argument is an operator makes it pretty obvious it's a macro. If you feel strongly, go ahead and change it.

In addition:

The rest of this post will deal with the construction of a macro that satisfies these requirements. If you don't want to wade through the tricks and traps involved with the creation of such a macro, then there's a download link near the end!

First attempt

Ok, so let's ignore the requirements to begin with to get something that does roughly what we want. We'll bash it in to shape afterward.

#define assert_relation(left, op, right) \
    do \
    { \
        if (!((left) op (right))) \
        { \
            std::cerr << __FILE__ << ':' << __LINE__ << ": " \
                         "failed assertion `" #left " " #op " " #right "', " \
                         "where lhs=" << (left) << ", rhs=" << (right) << '\n'; \
            abort(); \
        } \
    } \
    while(false)

For a first try it's not bad. We'll ignore the NDEBUG capability for now, as that's easy to add later on. Note that the abort() call is made inside the function in which the macro expansion occurs, so we haven't added any unnecessary layers to the stack trace. If you want to do something a bit fancier than abort() in the failure case, that's your call. I'll keep it simple for now.

The only real problem is that the operands (left and right) may be evaluated twice, meaning the values that are written to std::cerr may differ from those used in the comparison. This defeats the whole point of the macro.

So we somehow need to capture a textual representation of the operands while doing the comparison. Something like:

#define assert_relation(left, op, right) \
    do \
    { \
        if (!(capture_lhs(left) op capture_rhs(right))) \
        { \
                std::cerr << __FILE__ << ':' << __LINE__ << ": " \
                             "failed assertion `" #left " " #op " " #right "', " \
                             "where lhs=" << captured_lhs() << ", rhs=" << captured_rhs() << '\n'; \
                abort(); \
        } \
    } \
    while (false)

Here, I've assumed that we have previously created some additional functions capture_lhs() and capture_rhs() that return their arguments after storing them somewhere for later retrieval by captured_lhs() and captured_rhs(). However, how do we store a value of arbitrary type somewhere? Perhaps capture_lhs() and capture_rhs() could be template functions and save the value of their arguments to a string using a stringstream?

That will certainly work, but we don't really want to be doing all that work. It's fine when the condition fails and we'll be stopping the program anyway, but I'd really rather avoid it in the case where the comparison holds.

Trickery

We appear to have come up against a brick wall, here, but luckily there's a solution. Instead of having capture_lhs()/capture_rhs() return their argument, we'll instead have them return an object that holds a copy of their argument and overload the comparison operators to return an object that we can use to print an error message.

In other words, the actual line containing the comparison becomes something like:

// ...
if (!(mkop(left) op mkop(right)).check())
    abort();
// ...

mkop() is actually a template function:

template<typename T>
struct operand
{
    operand(T it) : it(it) { }
    T it;
};

template<typename T>
operand<T> mkop(T it) { return operand<T>(it); }

and we overload the comparison operators:

template<typename Lhs, typename Rhs>
struct comparison
{
    comparison(Lhs lhs, Rhs rhs, bool result) :
        lhs(lhs),
        rhs(rhs),
        result(result)
    {
    }

    // returns fals if the comparison failed
    bool check() { /* ... */ } 

    Lhs lhs;
    Rhs rhs;
    bool result;
};

template<typename Lhs, typename Rhs>
comparison<Lhs, Rhs> operator < (operand<Lhs> lhs, operand<Rhs> rhs)
{
    return comparison<Lhs, Rhs>(lhs.it, rhs.it, lhs.it < rhs.it);
} 

// and the same for <=, >, >=, == and !=
// ...

The body of comparison::check() now prints the error message when the comparison fails i.e. when its result member is false. We also need to give it some extra arguments to take __FILE__, __LINE__ and a text-based representation of the comparison, but that's easy enough.

Demo

It seems silly to drag this out any further, so I'll just present a quick demo and then give you the download containing the final macro I came up with, including support for NDEBUG, namespace protection and all the other bells and whistles.

#include "assert_relation.hpp"

int main()
{
    assert_relation(1, ==, 1); // basic test
    int i = 0;
    assert_relation(++i, <, 2); // operands evaluated once?

    int j = 1;
    assert_relation(i, !=, j); // BOOM!

    return 0;
}
eddmac:guff developer$ g++ assert_relation.cpp -o assert_relation -W -Wall -ansi -pedantic
eddmac:guff developer$ ./assert_relation
assert_relation.cpp:10: failed assertion `i != j', where lhs=1, rhs=1
Abort trap

Lovely!

Downloads

Comments

Oather

[23/09/2009 at 02:01:00]

Can't wait until the "auto" keyword becomes widely supported.

Then we can presumably simplify this down to something like:

#define assert_relation(left, op, right) \
    do \
    { \
        auto lres = (left)); \
        auto rres = (right)); \
        if (!(lres op rres)) \
        { \
            std::cerr << __FILE__ << ':' << __LINE__ << ": " \
                         "failed assertion `" #left " " #op " " #right "', " \
                         "where lhs=" << lres << ", rhs=" << rres << '\n'; \
            abort(); \
        } \
    } \
    while(false)

Sebastian

[04/10/2009 at 02:43:00]

Very nice solution. =)

The Software Purist

[25/11/2009 at 05:44:00]

This is a cute solution. I definitely like it. I have noticed similar issues with the usage of assert before. An assert fails, but you don’t always have enough information to debug it. I can certainly see a solution like this helping to alleviate that. Nice work.

(optional)
(optional)
(required, hint)

Links can be added like [this one -> http://www.mr-edd.co.uk], to my homepage.
Phrases and blocks of code can be enclosed in {{{triple braces}}}.
Any HTML markup will be escaped.