Friday 24 May 2013

Logger - Automatic selection of implementation on compilation

Ah, redesign. Loads of fun, hey? So... what have I decided to change?

1. I'll reintroduce logging levels to the interface. However, I won't be keeping its state, I'll rely on the Logger implementation. The interface will be basic - a method that returns true or false for a particular logging level, something like:

if (log.IsDebug())
{
    // LET'S DEBUG, PUT ON YOUR RED... SHOES??
}


That way, we'll avoid creating a lovely - and, sometimes, expensive - log string for nothing.

Why had I removed this from the interface, in my previous redesign? Call it the allure of simplicity. Now, I'm reminded - again - that for every problem, there is a solution that is relatively easy, relatively simple, and relatively wrong. That's relativity for you.

2. I'll add a const char * to my Logger class. I'll initialize it with __FILE__, in the ctor. That means adding a second parameter to Logger's ctor, with nullptr as default. Since Logger's typical usage pattern is as a local, ownership is not a concern.

3. I'll add an int parameter to all logging methods. I plan to use it to pass __LINE__.

Points 2 and 3 got me thinking that I might be imposing an unnecessary burden on anyone wanting to use these classes with their own logger implementation. I probably am, by forcing them to implement an interface where they must have two params, instead of just one, even if they don't use it.

E.g.,

// My little lib
log.Debug("My little debug message", __LINE__);


Now, someone comes along and decides to use my little lib code, but supplying his own logger. Alas, his logger falls a little short on the int param department, and only has Debug(string). In order for this to build, he now has to add a useless default int parameter to his implementation. Once again, not earth-shattering, but can we avoid it?

Yes, we can (no relation), and that's what I've been doing all these days, figuring out how. Why all this time? Well...

On my previous post, I've mentioned my puny specialization trick as "template meta-programming" (TMP). I knew I it was a bit of an aggrandizement; I just didn't know how big this "bit" was, i.e., how puny my little trick was, when compared to proper TMP. And these past few days have provided a fantastic eye-opener, even though I've barely scratched the surface, as far as this theme goes.

You see, the obvious way to keep my code from imposing unnecessary requirements on any client app is to allow it to fall back to a "minimum common interface". Something like this:

template <bool condition, typename LoggerImpl>
void LoggerBridge<condition, LoggerImpl>::Debug(
    std::string const& msg, int line)
{
    if (LoggerImpl_has_full_interface)
    {
        l.Debug(msg, file, line);
    }
    else
    {
        l.Debug(msg);
    }
}


And, obviously, I'd like the if (LoggerImpl_has_full_interface) bit to be as automatic as possible.

The solution? SFINAE and definitely more TMP than am I used to. I have a POC ready, and I'm finishing the tests, before I post it here, and include it in my LoggerBridge/Logger.

As a final note, it took me longer because I didn't want just any solution. I chose a solution that I can understand. I've come across many others that worked, but weren't as intuitive as the one I selected.

I've chosen this one, from here:

template<typename T, typename RESULT, typename ARG1, typename ARG2, typename ARG3>
class HasDebug
{
    template <typename U, RESULT (U::*)(ARG1, ARG2, ARG3)> struct Check;
    template <typename U> static char checkFn(Check<U, &U::Debug> *);
    template <typename U> static int checkFn(...);
public:
    enum { value = sizeof(checkFn<T>(0)) == sizeof(char) };
};


I can understand how this works, and I've managed to change it to create versions for const and static members (even though I'm still trying to figure out how the static version works).

With C++11 there are different ways of doing this, but I have to study those more closely. Like I said, this one is intuitive, works for all scenarios I imagined, and I understand it.

Also, this is not a subject where I plan to invest much of my time at this point. I can understand the concepts, and I believe they're extremely useful, but this is probably not the best way to realize their potential in a programming language. C++ should have a simpler way to achieve this.

So, let me get the Logger code ready, and - assuming no further problems - I'll explain the solution on my next post. If you're familiar with SFINAE, it'll all be old news for you.

No comments:

Post a Comment