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

в 4:56, , рубрики: c++, c++11, templates

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


Выбор нужного варианта лога, как мне показалось, лучше сделать через структуры-тэги.
Тэги:

struct LogFile{};
struct LogConsole{};
struct LogStream{};

А не сделать ли класс лога наследуемым от класса-инициализатора? Класс-инициализатор выбирается тэгом.

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

Естественно, тут в игру вступает частичная специализация инициализатора.

template<class LogType>
class initializator{};

template<>
class initializator<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.

template<>
class initializator<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.

template<>
class initializator<LogFile> : SipAppender<std::ofstream>
{
public:
    initializator() { _w.open("log.txt"); }
    ~initializator() { _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 initializator<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 базового класса. Проблема в том, что не у каждого класса есть соответствующие функции.
Можно было бы, наверное, решить проблему с помощью еще одного класса, с соответствующими виртуальными функциями и наследовать инициализаторы еще и от него, но мне не хотелось за всеми классами-инициализаторами таскать дополнительный интерфейс.
Тем более, если функции логически не связаны между собой, то странно запихивать их в один интерфейс. Так или иначе, было решено написать макрос, который определял бы структурку, которая разруливала вопрос о существовании функции у класса.

Сам макрос

#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.
Так же, традиционно скажу, что такой код меня пока устраивает, но возможно есть способ изящнее.

Автор: dandemidow

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js