📜 ⬆️ ⬇️

Logger with member functions that do not exist

I had in one project a class wrapper over log4cpp. One day I decided that I no longer like its interface, I decided to redo it a bit, then I redid a little more. Then the thought came to me, why not use generic programming? And then it started to turn around ... and all I needed was a variable logging behavior, that is, output to the screen or to a file or somewhere else depending on the type selected.


Choosing the right log option, it seemed to me, is better done through policy.
class LogFile class LogConsole class LogStream 


The log class is inherited from one of these policies.
')
 template<class LogType> class Logger : public <LogType> 


Classes themselves that determine the behavior of the logger

 class LogConsole { public: void do_log(log4cpp::LoggingEvent ev) { std::cout<<ev.timeStamp.getSeconds()<<" " <<log4cpp::Priority::getPriorityName(ev.priority)<<" " <<ev.categoryName<<" "<<ev.ndc<<": "<<ev.message<<std::endl; } }; 


This class simply displays messages on the screen. There is little use for log4cpp, well, just a little log4cpp :: LoggingEvent.

 class LogStream : SipAppender<std::ostringstream> { public: std::string do_buffer() const { return _w.str(); } void do_clear() { _w.str(std::string()); } void do_log(log4cpp::LoggingEvent ev) { appender.doAppend(ev); } }; 


The class works with ostringstream, displays nothing on the screen, displays messages on demand, by calling the do_buffer function.

 class LogFile : SipAppender<std::ofstream> { public: LogFile() { _w.open("SIP_LOG.txt"); } ~LogFile() { _w.close(); } void do_log(log4cpp::LoggingEvent ev) { appender.doAppend(ev); } }; 


The class adds events to a file, when creating such a log, the file must be created, when destroying the logger, the file must be closed, all this is described in the constructor and destructor, respectively.
One, detail, all function names begin with do_.

Since our classes are in fact wrappers over log4cpp, some must inherit from Appender, which contain a stream and an object that adds events to this stream, Appender provides these objects to the heir.

 template<class Writer> struct SipAppender { Writer _w; log4cpp::OstreamAppender appender; SipAppender() : appender(log4cpp::OstreamAppender("logger", &_w)){} }; 


Now directly a class of a roger Let it be singleton. Simplest. Especially since c ++ 11 gives us this opportunity.

 template<class LogType> class Logger : public <LogType> { DEFAULT_FUNC(do_buffer) DEFAULT_FUNC(do_clear) Logger() = default; static Logger& instance() { static Logger theSingleInstance; return theSingleInstance; } void log(log4cpp::Priority::PriorityLevel p, const std::string &msg) { this->do_log(log4cpp::LoggingEvent("CATEGORY",msg,"NDC",p)); } public: static void debug(const std::string ¶m){ instance().log(log4cpp::Priority::DEBUG, param); } static void info(const std::string ¶m){ instance().log(log4cpp::Priority::INFO, param); } static void error(const std::string ¶m){ instance().log(log4cpp::Priority::ERROR, param); } static std::string buffer() { return _do_buffer<Logger>::_do(&instance(), [](){return std::string();}); } static void clear() { _do_clear<Logger>::_do(&instance(), []()->void{}); } Logger& operator=(const Logger&) = delete; }; 


Here is all that is needed for proper functioning. The instance function is declared private, because the first is that I don’t want to give access to the logger object itself, and I don’t want to write when instance is called.
The benefit of the interface is small, all functions can be made static.

With the functions debug, info, error everything is clear, they call instance, log with priority and message.
In the functions buffer and clear there is a certain anomaly, as you may have noticed, it is associated with the macros DEFAULT_FUNC.
The idea is that buffer (outputting the contents of the log buffer) should call the base class do_buffer. The problem is that not every class has corresponding functions.
It would probably be possible to solve the problem with the help of another class, with the corresponding virtual functions, and inherit policy from it, but I didn’t want to carry an additional interface behind all the-policy classes.
Moreover, if the functions are logically unrelated, it is strange to push them into one interface. One way or another, it was decided to write a macro that would define a structure that would resolve the question of the existence of a function in a class.

Macro itself

 #define DEFAULT_FUNC(name) \ template<class T, class Enable = void> \ struct _##name \ { \ template<class DF> \ static auto _do(T *, DF df) -> decltype(df()) { return df(); } \ template<class DF> \ static auto _do(const T *, DF df) -> decltype(df()) { return df(); } \ }; \ template<class T> \ struct _##name <T, typename std::enable_if<std::is_member_function_pointer<decltype(&T::name)>::value>::type > \ { \ template<class DF> \ static auto _do(T *obj, DF df) -> decltype(df()) { (void)(df); return obj->name(); } \ template<class DF> \ static auto _do(const T *obj, DF df) -> decltype(df()) { (void)(df); return obj->name(); } \ }; 


As you can see, here the structure is defined using the SFINAE structure _ ## name (for the do_buffer function, the structure is called _do_buffer), if the name function is a member function T, then a second structure is defined that honestly performs this function for the object T, which is passed in the static function _do .
The function’s belonging to the class T defines std :: is_member_function_pointer <decltype (& T :: name)>. Magic.
If the function does not belong to the class, then the functor is executed, which is passed in the same _do function.
The function is overloaded for the case if the object T is transmitted constant. Having a little experimented, stopped on such option.

Thus, it can protect against annoying compilation errors, if the necessary function is suddenly not found in the template base class. And ensures the compatibility of the logging algorithm, if I need to suddenly change the output method.

For example, for such a code:

 using TestType = LogConsole; int main() { Logger<TestType>::info("Start log"); Logger<TestType>::error("Middle log"); Logger<TestType>::debug("End log"); std::cout<<Logger<TestType>::buffer()<<std::endl; Logger<TestType>::clear(); std::cout<<"clear: "<<std::endl; std::cout<<Logger<TestType>::buffer()<<std::endl; return 0; } 


It is guaranteed that it will work at any TestType.
Also, traditionally I will say that this code suits me so far, but perhaps there is a more elegant way.

UPD
Due to the critical assessment of readers, it became obvious that the method is not perfect, the macro does not take into account the existence of overloaded functions and arguments. It made me change the code a bit.

A separate macro for simple functions without an argument.
 #define D_FUNC_VOID(name) \ template<class S, class R, class DEnable = void> \ struct __##name \ { \ template<class DF> \ static R _do(S *, DF df) { return df(); } \ template<class DF> \ static R _do(const S *, DF df) { return df(); } \ }; \ template<class S, class R> \ struct __##name <S, R, typename std::enable_if<std::is_same<std::decltype(declval<S>().name()), R>::value>::type > \ { \ template<class DF> \ static R _do(S *obj, DF df) { (void)(df); return obj->name(); } \ template<class DF> \ static R _do(const S *obj, DF df) { (void)(df); return obj->name(); } \ }; 


Separately for functions with an argument
 #define D_FUNC_ARG(name) \ template<class S, class A, class R, class DEnable = void> \ struct __##name \ { \ template<class DF> \ static R _do(S *, A a, DF df) { return df(a); } \ template<class DF> \ static R _do(const S *, A a, DF df) { return df(a); } \ }; \ template<class S, class A, class R> \ struct __##name <S, A, R, typename std::enable_if<std::is_same<decltype(std::declval<S>().name(std::declval<A>())), R>::value>::type> \ { \ template<class DF> \ static R _do(S *obj, A a, DF df) { (void)(df); return obj->name(a); } \ template<class DF> \ static R _do(const S *obj, A a, DF df) { (void)(df); return obj->name(a); } \ }; 


General macro to determine which function to call.
 #define D_FUNC(name) \ template<class T, class Arg = void, class Ret = void, class Enable = void> \ struct _##name \ { \ D_FUNC_ARG(name) \ template<class DF> \ static Ret _do(T *obj, Arg a, DF df) { return __##name<T, Arg, Ret>::_do(obj, a, df); } \ }; \ template <class T, class Arg, class Ret> \ struct _##name<T, Arg, Ret, typename std::enable_if<std::is_void<Arg>::value>::type> \ { \ D_FUNC_VOID(name) \ template<class DF> \ static Ret _do(T *obj, DF df) { (void)(df); return __##name<T, Ret>::_do(obj, df); } \ }; 


Due to this, all functions that must be “protected” are substituted into the macro D_FUNC.

Then such calls are possible.
 _do_log<Logger, log4cpp::LoggingEvent>::_do(&instance(), log4cpp::LoggingEvent("CATEGORY",msg,"NDC",p), [](log4cpp::LoggingEvent){}); _do_buffer<Logger, void, std::string>::_do(&instance(), [](std::string){ return std::string();}); 

Yes, indeed, this is a very simple example for using this hell, but the interest was whether it was possible to realize this possibility.

UPD
With the most recent changes (D_FUNC), this would be the Logger class.
 template<class LogType> class Logger : public LogType { D_FUNC(do_clear) D_FUNC(do_buffer) D_FUNC(do_log) Logger() = default; static Logger& instance() { static Logger theSingleInstance; return theSingleInstance; } void log(log4cpp::Priority::PriorityLevel p, const std::string &msg) { _do_log<Logger, log4cpp::LoggingEvent>::_do(&instance(), log4cpp::LoggingEvent("CATEGORY",msg,"NDC",p), [](log4cpp::LoggingEvent){}); } public: static void debug(const std::string ¶m){ instance().log(log4cpp::Priority::DEBUG, param); } static void info(const std::string ¶m){ instance().log(log4cpp::Priority::INFO, param);} static void error(const std::string ¶m){ instance().log(log4cpp::Priority::ERROR, param);} static std::string buffer() { return _do_buffer<Logger, void, std::string>::_do(&instance(), [](){ return std::string();}); } static void clear() { _do_clear<Logger>::_do(&instance(), [](){}); } Logger& operator=(const Logger&) = delete; Logger(const Logger&) = delete; }; 

Source: https://habr.com/ru/post/242869/


All Articles