Sunday 26 May 2013

Logger - SFINAE solution for implementation selection

Before we continue, a quick recap:
  • We have some reusable code (a lib).
  • We want to add logging to it.
  • We want to allow the client app to replace the logger implementation.
In order to achieve the goal stated in the last point, we defined a minimum interface that the client app's logger will have to implement, and we'll now introduce a mechanism to make our lib decay into that interface, if the client app's logger doesn't implement our optimal interface.

As stated last time, we have this (slightly modified):

template<typename T, typename RESULT, 
    typename ARG1, typename ARG2, typename ARG3>
class DoesDebug
{
    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 { Exist = sizeof(checkFn<T>(0)) == sizeof(char) };
};


This will be our basic workhorse, and we'll need one for each member function.

We'll put it to work with this:

DoesDebug<Logger, void, std::string const&, char const*, int>::Exist

Upon finding this expression, the compiler will generate DoesDebug like this:
  • typename T = Logger
  • typename RESULT = void
  • typename ARG1 = std::string const&
  • typename ARG2 = char const*
  • typename ARG3 = int
Now, the compiler must calculate DoesDebug<etc...>::Exist. That means, resolving checkFn<Logger>(0).

There are two candidates for this:
char checkFn(Check<U, &U::Debug> *);
int checkFn(...);

The second candidate will always be the last option for the compiler, because of its "..." argument. As for the first, it must instantiate the Check struct, like this:

template <Logger, void (Logger::*)(std::string const&, 
   char const*, int)> struct Check;

The compiler will only be able to instantiate this if &Logger::Debug can be matched with this member function pointer: void (Logger::*)(string const&, char const*, int). So, there must be an overload of Logger::Debug() that has this signature. If that is the case, the compiler successfully instantiates Check, which means sizeof(checkFn<T>(0)) is char, because that's the return type of the first overload of CheckFn; that means Exist is true, because:

Exist = sizeof(char) == sizeof(char)

On the other hand, if there is no Logger::Debug() with the required signature, the compiler can't instantiate struct Check, which means it can't instantiate the first checkFn overload, and it'll go with the second. This means sizeof(checkFn<T>(0)) is int, and Exist is false.

Hence, we have a way to check whether our lib's optimal interface exists. What we need now is a way to use it.

template <bool condition, typename LoggerImpl>
class LoggerBridge
{
public:
...
    void Debug(std::string const& msg);
 

    template <typename LI = LoggerImpl>
    typename std::enable_if<DoesDebug<LI, void, std::string const&, 
        char const*, int>::Exist, void>::type
    Debug(std::string const&, char const*, int);
 

    template <typename LI = LoggerImpl>
    typename std::enable_if<!DoesDebug<LI, void, std::string const&, 
        char const*, int>::Exist, void>::type
    Debug(std::string const&, char const*, int);

...
private:
    LoggerImpl l;
};

We'll use two identical versions of Debug(string const&, char const*, int), but using enable_if with opposed conditions (DoesDebug<> and !DoesDebug<>) ensures only one will be generated by the compiler.

These functions then become:

template <bool condition, typename T> template <typename LI>
typename std::enable_if<DoesDebug<LI, void, std::string const&,  
    char const*, int>::Exist, void>::type
LoggerBridge<condition, T>::Debug(const std::string& msg, char const* file, int line)
{
    l.Debug(msg, file, line);
}
 

template <bool condition, typename T> template <typename LI>
typename std::enable_if<!DoesDebug<LI, void, std::string const&, 
    char const*, int>::Exist, void>::type
LoggerBridge<condition, T>::Debug(const std::string& msg, char const*, int)
{
    l.Debug(msg);
}


So, when DoesDebug is true, i.e., when the method LoggerImpl::Debug(string const&, char const*, int) exists, the compiler will use the first function; otherwise, it uses the second, which logs the message and ignores the remaining arguments. How? The template enable_if only has a type member when the condition is true. So, when the condition is false, type does not exist, which means the compiler excludes it from overload selection.

You'll notice the functions became template functions. My first version didn't work, and after some searching, I arrived here, where Johannes Schaub - litb answer explained why:
That's because when the class template is instantiated (which happens when you create an object of type Y<int> among other cases), it instantiates all its member declarations (not necessarily their definitions/bodies!).
(...)
You need to make the member templates' enable_if depend on a parameter of the member template itself.
So, the problem is that when the Debug() functions weren't templated, the compiler instantiated all of them when LoggerBridge was instantiated. That means both versions of Debug(string const& msg, char const* file, int line) were instantiated, and GCC complained that one could not be overloaded with the other. So, I changed the member functions to templates, in order to do this: "make the member templates' enable_if depend on a parameter of the member template itself".

After getting it to work with GCC, it was time to move over to MSVC. And solve some more problems, obviously. Actually, it was just one problem - warning/error C4519 default template arguments are only allowed on a class template, because of this:
Default template arguments for function templates     No     No
The two Nos refer to VC10 and VC11. So, I gathered, if I can't use default template arguments, I'll have to use them explicitly:

#ifdef _MSC_VER 
    template <typename LI>
#else
    template <typename LI = LoggerImpl>
#endif
    typename std::enable_if<DoesDebug<LI, void, std::string const&,
        char const*, int>::Exist, void>::type
    Debug(std::string const&, char const*, int); 

And, on the call site:

#ifdef _MSC_VER 
    t.Debug<Logger>("String", __FILE__, __LINE__);
#else
    t.Debug("String", __FILE__, __LINE__);
#endif


And there you go. I'm now checking how to hide this from the user, but here I'll probably cop out and use a #define. I'll use macros for creating the logger and calling the methods, anyway; there's no way I'm going to manually write all those __FILE__s and __LINE__s.

A few closing notes.
  • As I said, I'm adapting my Logger bare-bones example for this, and one thing I've already realized is the amount of boilerplate code is huge. It doesn't really bother me that much, because it's work I'll only do once. But it's still a lot more "code" than I expected.
  • All of the above may be obvious to anyone experienced in C++. However, what you read here reflects my first serious contact with templates, the previous contacts being the usual Oh-templates-are-very-useful-look-how-easily-you-can-create-a-container. And this was definitely my first contact with SFINAE. Two weeks ago, all I knew was I needed a way to select function implementations at compile time and that my cursory observation of enable_if, many months ago, showed it could be done. I'm infinitely grateful to all the folks throughout the web (on StackOverflow, on other programming forums, and on their personal sites/blogs) for taking the time to explain these concepts.
  • This solution seems better than what I have here, as far as boilerplate is concerned, but I didn't understand it completely. It's on my study list.

No comments:

Post a Comment