1

So basically I'm looking for a way to create an interface with various implementations for different platforms. Normally this would be quite straight forward, but I'm wanting to create an interface for a logger, and being able to have functions such as below is my goal:

class Log {
public:
    template<typename ...Args>
    virtual void log(const char* fmt, const Args&... args) const = 0;

    template<typename ...Args>
    virtual void warn(const char* fmt, const Args&... args) const = 0;

    template<typename ...Args>
    virtual void error(const char* fmt, const Args&... args) const = 0;
};

Now obviously this doesn't work as you can't have pure virtual templated functions.

One approach I saw was to make the Log class a CRTP (curiously recurring template pattern) class and use something like the following to get the desired behavior:

//Log.h (template<typename T> class Log {...})
template<typename ...Args>
void log(const char* fmt, const Args&... args) const {
    //the following line would essentially downcast the 'this' pointer and
    //call it's version of log instead...
    reinterpret_cast<T*>(this)->log(fmt, args...);
}

//ExampleLog.h (class ExampleLog : public Log<ExampleLog> {...})
template<typename ...Args>
void log(const char* fmt, const Args&... args) const {
    m_logger->log(fmt, args...);
}

The downside is that this make the interface awkward to use, constantly needing to use Log<Impl> where the Impl(implementation) may not be known/exposed.

I'm really lost on how I can go about creating an interface that includes function which have a variable number and type of parameters... ?

Hex Crown
  • 753
  • 9
  • 22

2 Answers2

2

Whether you have an error, a warning, or something else, it all boils down to a single text string that gets logged. Your virtual functions only need to take just that single std::string as a parameter, and all these functions in your Log base class will take care of formatting the parameters into a single std::string then invoking the real virtual functions:

class Log {
public:
    template<typename ...Args>
    void log(const char* fmt, Args && ...args) const
    {
       do_log(do_format(std::forward<Args>(args)...));
    }

    template<typename ...Args>
    void warn(const char* fmt, Args && ...args) const
    {
       do_warn(do_format(std::forward<Args>(args)...));
    }

    template<typename ...Args>
    void error(const char* fmt, Args && ...args) const
    {
       do_error(do_format(std::forward<Args>(args)...));
    }

private:

    template<typename ...Args>
    inline std::string do_format(const char *fmt, Args && ...args)
    {
         // Your homework assignment goes here
    }

    virtual void do_log(const std::string &) const = 0;

    virtual void do_warn(const std::string &) const = 0;

    virtual void do_error(const std::string &) const = 0;
};

So now all that's left to do here is implement do_format(). This is something you would've had to do anyway, in your logging class, but it needs to be done here, effectively type-erasing all templates parameters and replacing them with a single std::string. In the event that your logging function comes down to logging something other than a std::string, then construct whatever it is here, and that's what gets tossed into your virtual functions.

As to the reason why your variadic template parameters should use && rather than a const &, that subject is covered fairly extensively elsewhere, where the topic is called "perfect forwarding", so I refer you to those numerous stackoverflow and Google search results for more details.

Also, as covered fairly extensively elsewhere, you must declare and define do_format() inline.This will result in noticable code bloat. There are various techniques for reducing the code bloat, but that, again, is another topic for discussion.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • In my case I was wondering, in the event that say each library for each platform handled the formatting the same, it wouldn't make sense to reinvent the wheel. That said, I'm torn on which answer to accept this answer as the other one allows for the library to handle the formatting, while this one is probably better as its more flexible in allowing any logging library to work... rather than just ones that support the exact same formatting... :/ – Hex Crown Jul 20 '19 at 21:25
1

So basically I'm looking for a way to create an interface with various implementations for different platforms.

So you might create same class but with different implementation with different files, whereas you use only the right one according to the platform/OS/... or via predefined macro.

Something like

log_windows.h

template<typename ...Args>
void log(const char* fmt, const Args&... args)
{
// Windows implementation
}

template<typename ...Args>
void warn(const char* fmt, const Args&... args)
{
// Windows implementation
}

template<typename ...Args>
void error(const char* fmt, const Args&... args)
{
// Windows implementation
}

log_nix.h

template<typename ...Args>
void log(const char* fmt, const Args&... args)
{
// *nix implementation
}

template<typename ...Args>
void warn(const char* fmt, const Args&... args)
{
// *nix implementation
}

template<typename ...Args>
void error(const char* fmt, const Args&... args)
{
// *nix implementation
}

log.h

#ifdef (_WIN32)
#include "log_windows.h"
#else
#include "log_nix.h"
#endif
Jarod42
  • 203,559
  • 14
  • 181
  • 302