Был у меня в одном проекте класс-обертка над 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 ¶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;
};
Здесь все что нужно для полноценного функционирования. Функция 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