Monday 12 May 2014

Logging - The argument for the stream interface

Of all the things mentioned in my "resurface" post, I've been dedicating attention to my "logging macro front-end". To recap, the goal is to abstract the user code from the logging implementation used, thus allowing to substitute for another logging implementation with no changes to the code.
 
I want something like this:
 
LOG_FUNCTION_MACRO(ERROR_DEFINE_MACRO, 
    "Blimey! I didn't expect the Spanish Inquisition!", 
    chiefWeapons, mill.Trouble(), 42);

This is then "translated" into whatever interface the logging implementation provides for logging. So, with Boost Log, this could become something like this:
 
BOOST_LOG_SEV(someLog, boost::log::trivial::severity_level::error) 
    << __FILE__ << __LINE__ 
    << "Blimey! I didn't expect the Spanish Inquisition!" 
    << chiefWeapons << mill.Trouble() << 42;
 
Or, using Poco Logger's stream interface:
 
if (someLogRef.rdbuf()->logger().error())
    someLogRef.error() << __FILE__ << __LINE__
        << "Blimey! I didn't expect the Spanish Inquisition!" 
        << chiefWeapons << mill.Trouble() << 42 << endl;
else (void) 0;
 
Since I prefer a stream interface, that's where I began my work. Then, I moved on to what I call the "concatenation interface", where everything is concatenated into a single string. This was my first attempt, a few months ago, when I was using Poco Logger, but wasn't aware of Poco LogStream. The reason I settled on this was because I'm not a fan of the "format string" interface (i.e., printf()-like).
 
So, starting from our logging macro:
 
LOG_FUNCTION_MACRO(ERROR_DEFINE_MACRO, 
    "Blimey! I didn't expect the Spanish Inquisition!", 
    chiefWeapons, mill.Trouble(), 42);

we'd have something similar to this:
 
poco_error(someLogRef, __FILE__ + __LINE__ 
    + "Blimey! I didn't expect the Spanish Inquisition!" 
    + chiefWeapons + mill.Trouble() + 42);

And while I always preferred the stream interface, as I worked more on concatenation, I became aware it was not just a matter of preference; the stream interface is vastly superior. What do I mean by "superior"?
 
Let's look at this:
 
someStream << __FILE__ << __LINE__
    << "Blimey! I didn't expect the Spanish Inquisition!" 
    << chiefWeapons << mill.Trouble() << 42
 
__FILE__ and "Blimey! etc..." are char* (I'll ignore constness here), and work right out of the box. Ditto for __LINE__ and 42. So, our wildcards here are mill.Trouble() and chiefWeapons; for the sake of argument, let's assume mill.Trouble() returns a float and chiefWeapons is a container that defines its own operator<<(). This means close to 84% of our logging line works with no work required on our part. Our only additional work is defining operator<<() for whatever type chiefWeapons happens to be. And we probably would define it anyway, since output to a stream is always a handy feature, IMHO.
 
Now, let's look at this:

__FILE__ + __LINE__
    + "Blimey! I didn't expect the Spanish Inquisition!" 
    + chiefWeapons + mill.Trouble() + 42
  
This is supposed to be concatenation; concatenation assumes some string type. Since none of these arguments is a string type, we'd need to convert them. That's not difficult, but the question is - where would the conversion occur?

We don't want to place it in the original logging line, because we want it to be interface-agnostic. However, that's the only place where we know the type of each argument; we certainly couldn't place it in our macro mechanism, because macro parameters have no type.

We could create a family of template functions for this, and solve our problem through specialization - if we pass a string type, just return the string itself (yes, I'm ignoring the several string types in C++ libs and the necessity to copy those into a single type); if we pass a char*, build a string with it; if we pass an int, call to_string() (or similar); etc, meaning, every type we use would need a way to convert to string. And, while we might not require a template specialization for each type (e.g., we could use to_string() for int, long, or float), we'd be pretty close to that mark.

So, assuming this could be pulled off, with a proper design, it's still more work than taking advantage of a group of core types that have an already-functioning operator<<(), and only adding this to other types that require it.

Then, there is the question of performance - unless we use some mechanism like QstringBuilder, concatenation will be much more expensive than streaming output.
 
Finally, there is one last point that makes me prefer the stream interface, one that I've come upon as my logging usage became more "complex" - no conversions necessary. Conversions are one of the sources of problems in C/C++, and while we can mark them as explicit, I prefer sticking to a simple rule of defining no unnecessary conversions. If I only need a conversion to string when I'm outputting to log, then it is an unnecessary conversion.
 
So, where does this leaves my "logging macro front-end"? I'm going to finish my work and publish it with only the stream interface functional. The concatenation interface will be semi-functional, meaning no provision for conversion to string. I realize this is mostly self-defeating, since this means I probably won't use it. But I have two good options with operator<<() at the moment, so I'll stick to it, for the time being.
 

No comments:

Post a Comment