Saturday, 11 June 2011

Comparison Operand Stripping in C++

If you're writing C++ assertions or unit tests, you'll have written code similar to:
ASSERT(a < b);
or
if (!VERIFY(a != b)) { ... }
If you're running without a debugger attached or with optimisations fully on, you'll get the usual error report, but no idea what the underlying values are or were:
test.cpp(1): Failed VERIFY(a != b)
Wouldn't it be wonderful if it did? Up until recently, I thought the only way of achieving that was to use ugly macros of the forms:
ASSERT_OP(a, <, b);
ASSERT_LT(a, b);
 which, if you're lucky, will produce output similar to:
test.cpp(2): Failed ASSERT(a < b)
    'a' is 321
    'b' is 123
The only problem is that it's extremely ugly. There must be a better way, surely? But it's C++: there's always an alternative, no matter how convoluted internally. Consider operator precedence:
something << a < b >> something
This will evaluate 'something << a' first, then 'b >> something', and then apply the less-than operation to the results. Now consider:
namespace Verify
{
    struct Outer;
    template<typename T>
    struct Inner
    {
        const T& value;
        explicit Inner(const T& inner) :
            value(inner) {}
    };
    template<typename T>
    Inner<T> operator<<(const Outer&,
                        const T& inner)
    {
        return Inner<T>(inner);
    }
    template<typename T>
    Inner<T> operator>>(const T& inner,
                        const Outer&)
    {
        return Inner<T>(inner);
    }
}
Note that 'Outer' is not defined, only declared; we're only going to use its type information:
#define CHILLIANT_OUTER \
    (*(Verify::Outer*)nullptr)
#define CHILLIANT_VERIFY(expr) /
    CHILLIANT_BLAH(CHILLIANT_OUTER << expr >>\
                   CHILLIANT_OUTER)
But what's the defintion of 'CHILLIANT_BLAH()'? Well, we want to report the location of any failure along with the expression text:
#define CHILLIANT_VERIFY(expr) /
    (Verify::Report(__FILE__,__LINE__,#expr, \
        (CHILLIANT_OUTER << expr >>\
         CHILLIANT_OUTER)))
The type of the final argument to 'Verify::Report()' can be one of three things:
  1. The result of a binary comparison (including encapsulations of the two operand values),
  2. A single value (and an encapsulation of that value) that can be utilised as a Boolean condition, and
  3. A Boolean value (for cases where we cannot easily determine the encapsulations).
The third case should be redundant, but for simplicity we're not going to handle complex cases such as 'a < b && c > d' here. That's left as an exercise for the reader.
namespace Verify
{
    template<typename L,typename R>
    bool Report(/*blah*/,
                const Binary<L,R>& result)
    {
        if (!result.condition)
        {
            /*blah*/
        }
        return result.condition;
    }
    template<typename T>
    bool Report(/*blah*/,
                const Unary<T>& result)
    {
        if (!result.condition)
        {
            /*blah*/
        }
        return result.condition;
    }
    bool Report(/*blah*/,
                bool result)
    {
        if (!result)
        {
            /*blah*/
        }
        return result;
    }
}
Now all that remains is the code to glue the three condition types to the three reporting functions. Firstly, binary comparison operations:
namespace Verify
{
    template<typename L,typename R>
    struct Binary
    {
        bool condition;
        const char* operation;
        const Inner<L>& lhs;
        const Inner<R>& rhs;
        Binary(bool result,
               const char* op,
               const Inner<L>& left,
               const Inner<R>& right) :
            condition(result),
            operation(op),
            lhs(left),
            rhs(right) {}
    };
    template<typename L,typename R>
    Binary<L,R> operator<(const Inner<L>& lhs,
                          const Inner<R>& rhs)
    {
        return Binary<L,R>(
            lhs.value < rhs.value,
            "<", lhs, rhs);
    }
    /* Similarly for <=, ==, !=, > and >= */
}
This will be invoked for expressions such as 'x < y' and even 'x*2 < y+1'. But we also want to handle expressions without comparison operators:
namespace Verify
{
    template<typename T>
    struct Unary
    {
        bool condition;
        const Inner<T>& value;
        Unary(bool result,
              const Inner<T>& inner) :
            condition(result),
            value(inner) {}
    };
    template<typename T>
    Unary<T> operator>>(const Inner<T>& inner,
                        const Outer&)
    {
        return Unary<T>(inner.value, inner);
    }
}
Finally, we must deal with expressions that use operators that "fall between the gaps" (i.e. have lower precedence than the comparison operators, such as '&&'). A good catch-all is to allow 'Inner<T>' to implicitly convert to its underlying type 'T':
namespace Verify
{
    template<typename T>
    struct Inner
    {
        const T& value;
        explicit Inner(const T& inner) :
            value(inner) {}
        operator T() const { return value; }
    };
}
Adding type-safe helper functions to help with reporting and putting everything together, we end up with the following:
namespace Verify
{
    void Output(const char* fmt, ...);
    struct Outer;
    template<typename T>
    struct Inner
    {
        const T& value;
        explicit Inner(const T& inner) :
            value(inner) {}
        operator T() const { return value; }
        void Output(const char* label) const;
    };
    template<typename T>
    Inner<T> operator<<(const Outer&,
                        const T& inner)
    {
        return Inner<T>(inner);
    }
    template<typename T>
    Inner<T> operator>>(const T& inner,
                        const Outer&)
    {
        return Inner<T>(inner);
    }
    template<typename T>
    struct Unary
    {
        bool condition;
        const Inner<T>& value;
        Unary(bool result,
              const Inner<T>& inner) :
            condition(result),
            value(inner) {}
    };
    template<typename T>
    Unary<T> operator>>(const Inner<T>& inner,
                        const Outer&)
    {
        return Unary<T>(inner.value, inner);
    }
    template<typename L,typename R>
    struct Binary
    {
        bool condition;
        const char* operation;
        const Inner<L>& lhs;
        const Inner<R>& rhs;
        Binary(bool result, const char* op,
               const Inner<L>& left,
               const Inner<R>& right) :
            condition(result),
            operation(op),
            lhs(left),
            rhs(right) {}
    };
    template<typename L,typename R>
    Binary<L,R> operator<(
        const Inner<L>& lhs,
        const Inner<R>& rhs)
    {
        return Binary<L,R>(
            lhs.value < rhs.value,
            "<", lhs, rhs);
    }
    template<typename L,typename R>
    Binary<L,R> operator<=(
        const Inner<L>& lhs,
        const Inner<R>& rhs)
    {
        return Binary<L,R>(
            lhs.value <= rhs.value,
            "<=", lhs, rhs);
    }
    template<typename L,typename R>
    Binary<L,R> operator>(
        const Inner<L>& lhs,
        const Inner<R>& rhs)
    {
        return Binary<L,R>(
            lhs.value > rhs.value,
            ">", lhs, rhs);
    }
    template<typename L,typename R>
    Binary<L,R> operator>=(
        const Inner<L>& lhs,
        const Inner<R>& rhs)
    {
        return Binary<L,R>(
            lhs.value >= rhs.value,
            ">=", lhs, rhs);
    }
    template<typename L,typename R>
    Binary<L,R> operator==(
        const Inner<L>& lhs,
        const Inner<R>& rhs)
    {
        return Binary<L,R>(
            lhs.value == rhs.value,
            "==", lhs, rhs);
    }
    template<typename L,typename R>
    Binary<L,R> operator!=(
        const Inner<L>& lhs,
        const Inner<R>& rhs)
    {
        return Binary<L,R>(
            lhs.value != rhs.value,
            "!=", lhs, rhs);
    }
    bool Report(const char* file, long line,
                const char* expression,
                bool result)
    {
        if (!result)
        {
            Output("%s(%ld): Failed "
                   "CHILLIANT_VERIFY(%s)\n",
                   file, line, expression);
            Inner<bool> value(false);
            value.Output("value");
        }
        return result;
    }
    template<typename T>
    bool Report(const char* file, long line,
                const char* expression,
                const Unary<T>& result)
    {
        if (!result.condition)
        {
            Output("%s(%ld): Failed "
                   "CHILLIANT_VERIFY(%s)\n",
                   file, line, expression);
            result.value.Output("value");
        }
        return result.condition;
    }
    template<typename L,typename R>
    bool Report(const char* file, long line,
                const char* expression,
                const Binary<L,R>& result)
    {
        if (!result.condition)
        {
            Output("%s(%ld): Failed "
                   "CHILLIANT_VERIFY(%s)\n",
                   file, line, expression);
            Output("    for "
                   "(LHS %s RHS)"
                   " where\n",
                   result.operation);
            result.lhs.Output("LHS");
            result.rhs.Output("RHS");
        }
        return result.condition;
    }
}
#define CHILLIANT_VERIFY_OUTER \
    (*(Verify::Outer*)nullptr)
#define CHILLIANT_VERIFY(expr) \
    (Verify::Report(__FILE__,__LINE__,#expr,\
        (CHILLIANT_VERIFY_OUTER << expr >>\
         CHILLIANT_VERIFY_OUTER)))
Now we can write reporters (including for user-defined types) and perform checks such as the following:
template<>
void Verify::Inner<bool>::
    Output(const char* label) const
{
    Verify::Output("   %s is %s (bool)\n",
                   label,
                   value ? "true" : "false");
}
template<>
void Verify::Inner<int>::
   
Output(const char* label) const
{
    Verify::Output("   %s is %d (int)\n",
                   label,
                   value);
}
template<>
void Verify::Inner<float>::
   
Output(const char* label) const
{
    Verify::Output("   %s is %g (float)\n",
                   label,
                   value);
}

void TestVerify(void)
{
    int i = 3;
    float f = 3.14159f;
    if (!CHILLIANT_VERIFY(false)) ...
    if (!CHILLIANT_VERIFY(i > 0 && i < 2)) ...
    if (!CHILLIANT_VERIFY(12 * 6 < i + 7)) ...
    if (!CHILLIANT_VERIFY(12 * 6 > f * 1000)) ...
}
This will produce the following output:
test.cpp(101): Failed CHILLIANT_VERIFY(false)
    value is false (bool)
test.cpp(102): Failed CHILLIANT_VERIFY(i > 0 && i < 2)
    value is false (bool)
test.cpp(103): Failed CHILLIANT_VERIFY(12 * 6 < i + 7)
    for CHILLIANT_VERIFY(LHS < RHS) where
    LHS is 72 (int)
    RHS is 10 (int)
test.cpp(104): Failed CHILLIANT_VERIFY(12 * 6 > f * 1000)
    for CHILLIANT_VERIFY(LHS > RHS) where
    LHS is 72 (int)
    RHS is 3141.59 (float)
The nice thing about this "comparison operand stripping" pattern is that you can turn the feature on and off at will, without jumping through syntactic hoops:
#define CHILLIANT_VERIFY(expr) (expr)
It can also be extended to assertions. What's not to like?

1 comment :

  1. This comment has been removed by a blog administrator.

    ReplyDelete