Destructuring Assertions
Assertions are a major tool in defensive programming and I consider it a symbol of a mature programmer when their code is liberally accompanied by assertions. They embody a fail-fast mentality and serve as additional documentation, making many assumptions explicit that the programmer made during the implementation.
#include <cassert>
float dot_product(std::span<float> lhs, std::span<float> rhs)
{
assert(lhs.size() == rhs.size());
auto sum = 0.f;
for (size_t i = 0; i < lhs.size(); ++i)
sum += lhs[i] * rhs[i];
return sum;
}
In this post we will remedy a shortcoming of the traditional C assertion:
Assertion `lhs.size() == rhs.size()' failed.
Okay, our assertion failed, our code (or assumption) is buggy.
But what are the sizes of lhs
and rhs
?
Test frameworks like Catch2 or doctest are (seemingly magically) able to display the values of lhs
and rhs
when their assertions / checking macros fail:
Example.cpp:7: FAILED:
REQUIRE( lhs.size() == rhs.size() )
with expansion:
100 == 300
Let’s assume we have an assertion of the form ASSERT(a == b)
.
The rest of this post explains how to display the values of a
and b
.
SPOILER: we’re going to exploit operator precedence and break some macro hygiene.
a == b
will be expanded toassert_t{} < a == b
, which is then parsed as(assert_t{} < a) == b
, allowing access toa
andb
.
Typical Assertion Anatomy
Before we start destructuring the assertion expression, let’s take a look at how assertions are typically implemented. A super naive version would be:
#define ASSERT(expr) if (!expr) \
on_assert_failed(#expr, __FILE__, __LINE__, __FUNCTION__);
However, looking at a standard library implementation we find something similar to
void on_assert_failed(char const* expr, char const* file, int line, char const* fun);
#define ASSERT(expr) (static_cast<bool>(expr) ? \
void(0) : \
on_assert_failed(#expr, __FILE__, __LINE__, __FUNCTION__))
There is already something noteworthy going on here. Most of this is basic macro hygiene but it cannot hurt to repeat it.
!expr
is dangerous as ASSERT(a == b)
would expand to if (!a == b)
.
The common fix of !(expr)
is slightly better but still dangerous as the additional parentheses silence warnings, e.g. for the typo in ASSERT(a = b)
.
A better solution is !static_cast<bool>(expr)
which preserves most warnings.
Secondly, in function-like macros, it is common courtesy to make them behave as if they were normal functions.
On the one hand, this means requiring a semicolon at the end.
On the other hand, it means that one should implement ASSERT
as an expression, not a statement.
// should error due to missing ;
ASSERT(a == b)
// should work as expected
if (some_condition)
ASSERT(a == b);
else
ASSERT(a != b);
Note that the else
would attach to the if (!expr)
of the naive version, NOT the expected if (some_condition)
.
This is the reason why expressions are preferred.
For the assertion the ternary operator cond ? true_expr : false_expr
is sufficient.
If you want to execute multiple statements, the do { ... statements ... } while(0)
construct is popular.
It is not an expression but at least it interacts properly with other control flow structures.
Destructuring Simple Expressions
So, now that we know how a basic assertion works, how do we “analyze” the asserted expression to get a
and b
in ASSERT(a == b)
?
The metaprogramming capabilities of C++ do not allow us to inspect arbitrary expression as for example Nim Macros are able to.
What can we do instead?
If ASSERT
were a normal function, a == b
would be evaluated before calling the function and there would be no chance to get the values of a
and b
.
However, we are in a macro setting where the expression is “embedded” into the macro body via token substitution.
While we cannot change a == b
, we can control its surroundings.
How does this help us?
Our goal is to “snatch” a
from a == b
, store its value AND string representation, then compare against b
, while also storing b
s string representation.
If the comparison fails, we call the “assertion failed” handler while passing the representation of a
and b
.
As already spoilered, we will exploit operator precedence.
We are going to surround a == b
with assert_t{} OP a == b
where assert_t
is a helper type and OP
is our “snatching” operator.
Comparisons are associated left-to-right, so OP
must have the same or higher precedence than the comparisons ==, !=, <, <=, >, >=
.
However, when its precedence is too high, it will interface with more complex assertions such as ASSERT(a + b == c)
where we want to “snatch” a + b
and not only a
.
Looking at the precedence table, this leaves us with the shift operators or <, <=, >, >=
(ignoring the C++20 <=>
spaceship).
For no particular reason I’ll continue with <
:
void set_assert_vars(std::string_view a, std::string_view b, std::string_view comp);
void on_assert_failed(char const* expr, char const* file, int line, char const* fun);
#define ASSERT(expr) ((assert_t{} < expr) ? \
void(0) : \
on_assert_failed(#expr, __FILE__, __LINE__, __FUNCTION__))
template <class A>
struct check_t
{
A a;
template <class B>
bool operator==(B&& b) const
{
if (a == b)
return true;
set_assert_vars(std::to_string(a), std::to_string(b), "==");
return false;
}
};
struct assert_t
{
template <class A>
check_t<A> operator<(A&& a)
{
// this code prevents copies
// if a is an lvalue ref, A is also an lvalue ref, e.g. int&
// if a is an rvalue ref, A is not a ref and a is moved into check_t
return check_t<A>{std::forward<A>(a)};
}
};
Consider for example ASSERT(1 + 1 == 3);
.
Inside the macro, this expands to the condition assert_t{} < 1 + 1 == 3
, which is parsed as (assert_t{} < 1 + 1) == 3
.
This calls operator<
of assert_t
, returning a check_t<int>
with member a
set to 2
.
check_t
in turn has an operator==
that is called with 3
as its right-hand side.
The comparison if (a == b)
fails, at which point set_assert_vars
is called.
Only now are a
and b
converted to strings.
This is important because to_string
is kinda expensive and we don’t want to slow down runtime performance when the assertion is not failing.
We know that the “assertion failed” handler will be called immediately afterwards, so set_assert_vars
can simply store its arguments in thread_local
global variables that the handler will then display.
See here for a fully working example.
ASSERT(1 + 1 == 3);
assertion '1 + 1 == 3' failed
in ./example.cpp:55 (main)
expansion: 2 == 3
Next Steps
To make this production-ready I would recommend the following:
- add all desired comparison operators to
check_t
- add a
operator bool()
tocheck_t
to supportASSERT(some_bool)
- add
static_assert
s tocheck_t
that check if the comparison betweenA
andB
actually works (nicer compile errors) - move
assert_t
andcheck_t
in “detail::
” or “impl::
” scopes - write a user-extensible version of
std::to_string
so that user types can register their own formatter - allow types without
to_string
(e.g. print???
) - try to remove the dependence on
<string>
as this is a rather expensive header (for example, the customto_string
might return achar const*
that was allocated vianew
and isdelete[]
d byon_assert_failed
; this is not performance critical) - add
operator&&
andoperator||
tocheck_t
andassert_t
that causestatic_assert
failures (we cannot destructure chained expressions so this should be forbidden. there is an escape hatch viaASSERT((a || b))
without destructuring) - only store a reference in
check_t
so that types must not even be movable (lifetime is fine as the reference doesn’t outlive the assert expression) - add an optional general message to the assertion, supporting a format-like syntax (e.g.
ASSERTF(a == f(b), "xyz is not fulfilled and b is {}", b);
) - proper integration with logging, stack traces, custom assert handlers
- optimize the performance so that assertions can also be enabled in
Release with Debug Info
mode (or evenRelease
) with minimal runtime impact
Summary
While the metaprogramming capabilities of C++ are not expressive enough to analyze expression ASTs, we nevertheless can achieve simple “destructuring” of comparisons to implement assertions that report the compared values:
auto a = 1;
auto b = 2;
auto c = 2;
auto d = 3;
ASSERT(a + b == c + d);
// assertion 'a + b == c + d' failed
// in ./example.cpp:55 (main)
// expansion: 3 == 5
This works by breaking macro hygiene and use operator precedence to “snatch” the compared value before it is actually compared.
ASSERT(a + b == c + d);
// is expanded to
((assert_t{} < a + b == c + d) ? void(0) :
on_assert_failed(...));
// is parsed as
(((assert_t{} < a + b) == c + d) ? void(0) :
on_assert_failed(...));
Additional discussion and comments on reddit.
(Title image from pixabay)