Optional streaming
Catch has a number of macros that allow values of
arbitrary types to be streamed into an ostringstream. The canonical example is
the INFO
macro:
INFO( "There were " << bottles.size() << " green bottles, hanging on the wall" );
This macro builds up a string that will be passed to the next assertion to be
included as an annotation. Note that, unlike with a naked ostringstream
there
is no leading <<
. This makes it clean and uncluttered when you just want to
log a single value (such as a string), for example:
INFO( "Weirdness" );
The obvious way to do this is for the macro to provide the leading <<
prior to
its argument. Conceptually something like this:
#define INFO( log ) { \
std::ostringstream oss; \
oss << log; \
useTheString( oss.str() );
}
This all works quite nicely. But there are a few other macros that use this
idiom, too: WARN
, SUCCEED
and FAIL
.
The last two are of interest because the logging behaviour is more of a
secondary concern. The primary behaviour is to appear like a passing or
failing assertion, respectively, without the need to actually assert on
anything. SUCCEED
can be useful if you otherwise have no assertions in a test
and you don't want to see warnings about it. FAIL
is useful if the situation
that leads to the failure is not captured in an expression for some reason. It
can also be useful to force a test to fail, perhaps as a placeholder. These
are useful macros to have available, but they are not often needed in
practice. So when they are it's nice to be able to annotate their useage
inline - hence the streamed argument.
This is all well and good. But I've found there are still enough cases where I don't want to annotate that having to pass an empty string or make something up is a little annoying. I also use a similar idiom in other projects where it would be nice to be able to make the stream completely optional.
This is not as easy as it sounds, though. The first, and most obvious, issue is that this requires support for variadic macros. Catch has made use of variadic macros, where available, for some time now. In theory they are available to any C++11 compiler. In practice most, if not all, compilers that support any reasonable chunk of C++11 support variadic macros - and most supported them as an extension even before that. That's certainly true of Visual C++, GCC and Clang.
The technically more interesting problem, though, is dealing with that initial
<<
. Remember the first <<
is being supplied inside the macro. It will still be
there even if the caller does not supply an argument to the macro. If we wrote
FAIL
the same way we presented INFO
earlier (but with variadic macros) it
might look something like this:
#define FAIL( ... ) { \
std::ostringstream oss; \
oss << __VA_ARGS__; \
notifyFail( oss.str() ); \
}
... which, with no argument provided, would expand to...
{
std::ostringstream oss;
oss << ;
notifyFail( oss.str() );
}
Do you see the problem? With nothing following the <<
this will not compile.
So do we need a different operator? What properties would we need? It seems
we'd need an operator that comes in two forms: a binary operator that allows
us to capture an argument, and a unary operator that allows us to omit the
argument. Furthermore the binary form must not require its argument to be
enclosed in any sort of brackets. Finally it must have higher precedence than
<<
so we can switch over to normal stream insertion at that point.
That's a long list. Does such an operator exist? Fortunately there's not just
one but two such operators to choose from! +
and -
. The only slight hitch is
that the unary form is right-to-left associative, whereas the binary form is
left-to-right. So how can we work these in?
Let's pick one of the operators. I've gone with +
, but I don't think there is
any advantage either way. Because unary +
is right-to-left associative it
needs to prefix something. So we can't use it at the start of our streaming
expression. We can, however, use it at the end. Then we'll need an object to
apply it to. The object doesn't actually need to do anything else. I've gone
with this implementation of StreamEndStop
in Catch:
struct StreamEndStop {
std::string operator+() {
return std::string();
}
};
With this definition the expression, +StreamEndStop()
now yields an empty
string - which is idempotent with a stringstream
. Which means we can write:
{
std::ostringstream oss;
oss << +StreamEndStop();
notifyFail( oss.str() );
}
And oss.str()
evaluates to an empty string. Perfect. But what about when we do
stream something? Well that would expand to:
{
std::ostringstream oss;
oss << something +StreamEndStop();
notifyFail( oss.str() );
}
... where something could be a string or variable or literal of any type. So we need some way for the expression:
something +StreamEndStop()
to yield the value of something. That's where the binary form of operator+
comes in:
template<typename T>
T const& operator + ( T const& value, StreamEndStop& ) {
return value;
}
Now, whether we supply nothing, a single value or multiple values joined by
<<
s we'll end up with a stringstream containing what we expect. The relevant
bit of code in Catch actually looks like this:
Catch::ExpressionResultBuilder( messageType ) \
<< __VA_ARGS__ \
+::Catch::StreamEndStop()
which yields an ExpressionResultBuilder that gets passed on elsewhere. This is
all protected by CATCH_CONFIG_VARIADIC_MACROS
. Otherwise it falls back to:
Catch::ExpressionResultBuilder( messageType ) << log
So a lot of work to save a few explicit empty strings, but sometimes it's the little things.