Pull to refresh

Logger с функциями-членами, которых нет

Reading time7 min
Views7.4K
Был у меня в одном проекте класс-обертка над log4cpp. В один прекрасный день посчитал, что его интерфейс перестал мне нравится, решил немного переделать, потом переделал еще немного. Потом мне пришла мысль, почему бы не использовать обобщенное программирование? И тут завертелось… а нужно мне было только вариативное поведение логирования, то есть, вывод на экран или в файл или еще куда либо в зависимости от выбранного типа.


Выбор нужного варианта лога, как мне показалось, лучше сделать через policy.
class LogFile
class LogConsole
class LogStream


Класс лога наследуется от одной из этих policy

template<class LogType>
class Logger : public <LogType>


Сами классы, определяющие поведение логера

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;
    }
};


Этот класс просто выводит сообщения на экран. Тут практически не используется log4cpp, ну, только немного 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);
    }
};


Класс работает с ostringstream, не выводит ничего на экран, выводит сообщения по требованию, вызовом функции do_buffer.

class LogFile : SipAppender<std::ofstream>
{
public:
    LogFile() { _w.open("SIP_LOG.txt"); }
    ~LogFile() { _w.close(); }

    void do_log(log4cpp::LoggingEvent ev)
    {
        appender.doAppend(ev);
    }
};


Класс складывает события в файл, при создании такого логера файл должен быть создан, при уничтожении логера — файл нужно закрыть, все это описывается в конструкторе и деструкторе соответственно.
Одна, деталь, все имена функций начинаются с do_.

Поскольку наши классы фактически являются обертками над log4cpp, то некоторые должны наследоваться от Appender, которые содержат поток stream и объект, добавляющий события в этот stream, Appender предоставляет эти объекты наследнику.

template<class Writer>
struct SipAppender
{
    Writer _w;
    log4cpp::OstreamAppender appender;

    SipAppender()
        : appender(log4cpp::OstreamAppender("logger", &_w)){}
};


Теперь непосредственно класс логера. Пусть это будет синглтон. Самый простой. Тем более, что c++11 дает нам такую возможность.

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 &param){ instance().log(log4cpp::Priority::DEBUG, param); }
    static void info(const std::string &param){ instance().log(log4cpp::Priority::INFO, param); }
    static void error(const std::string &param){ 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;
};


Здесь все что нужно для полноценного функционирования. Функция instance объявлена закрытой, поскольку, первое — не хочу давать доступ к самому объекту логера, и не хочу писать при вызове логера instance.
Благо интерфейс небольшой, все функции можно сделать статическими.

С функциями debug, info, error все понятно, они вызывают instance, log с приоритетом и сообщением.
В функциях buffer и clear есть некая аномалия, как вы уже могли заметить, связана она с макросами DEFAULT_FUNC.
По идее, buffer (вывод содержимого буфера лога) должен вызывать do_buffer базового класса. Проблема в том, что не у каждого класса есть соответствующие функции.
Можно было бы, наверное, решить проблему с помощью еще одного класса, с соответствующими виртуальными функциями и наследовать policy еще и от него, но мне не хотелось за всеми классами-policy таскать дополнительный интерфейс.
Тем более, если функции логически не связаны между собой, то странно запихивать их в один интерфейс. Так или иначе, было решено написать макрос, который определял бы структурку, которая разруливала вопрос о существовании функции у класса.

Сам макрос

#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(); } \
};


Как видете, здесь определяется с помощью SFINAE структура _##name (для функции do_buffer структура называется _do_buffer), если функция name является функцией членом T, то определена вторая структура, которая честно выполняет эту функцию для объекта T, который передается в статической функции _do.
Принадлежность функции классу T определяет std::is_member_function_pointer<decltype(&T::name)>. Магия.
Если же функция не принадлежит классу, то выполняется функтор, который передается в той же функции _do.
Функция перегружена для случая, если объект T передается константный. Немного поэкспериментировав, остановился на таком варианте.

Таким образом, это может защитить от раздражающих ошибок компиляции, если нужной функции вдруг не оказалось в шаблонном базовом классе. И обеспечивает совместимость алгоритма логгирования, если мне понадобится вдруг изменить способ вывода.

Например, для такого кода:

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;
}


Гарантируется, что он сохранит работоспособность при любом TestType.
Так же, традиционно скажу, что такой код меня пока устраивает, но возможно есть способ изящнее.

UPD
Благодаря критической оценке читателей, стало очевидно, что способ не идеален, макрос не учитывает существование перегруженных функций и аргументов. Это заставило меня немного изменить код.

Отдельный макрос для простых функций без аргумента
#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(); } \
};


Отдельно для функций с аргументом
#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); } \
};


Общий макрос для определения какую конкретно функцию вызвать.
#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); } \
};


Благодаря этому все функции, которые должны быть «защищены», подставляются в макрос D_FUNC.

Тогда возможны вот такие вызовы
_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();});

Да, действительно, это очень простой пример для использования этого ада, но интерес был в том, можно ли реализовать такую возможность.

UPD
С учетом последних изменений (D_FUNC) так мог бы выглядеть класс Logger
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 &param){ instance().log(log4cpp::Priority::DEBUG, param); }
    static void info(const std::string &param){ instance().log(log4cpp::Priority::INFO, param);}
    static void error(const std::string &param){ 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;
};
Tags:
Hubs:
Total votes 16: ↑12 and ↓4+8
Comments8

Articles