Pull to refresh

Мой велосипед к reflection в c++

Reading time 20 min
Views 6K
Вдохновившись публикацией «Logger с функциями-членами, которых нет», решил выдать на всеобщее обозрение свой мета-велосипед для рефлекшена, которым вполне успешно пользуюсь, и не только для логгирования. Но начнем всё же с простого логгирования, продолжая вышеупомянутую публикацию.

При реализации логгирования, задачи для себя были поставлены следующие:

  • Решить задачу на «мета-уровне», чтобы быть отвязанным от конечной реализации
  • Фронтенд апи для логгирования должен быть простым и прозрачным
  • Иметь возможность выключать ненужные уровни логгирования на этапе компиляции одной константой; например: все что выше LOG_NOTICE не должно попасть в результирующий бинари

Фронтенд выглядит так:

1. В конструкторе CConnection мы логгируем:
TestLog::Log<LOG_NOTICE>() << *this << "connection created";

2. Где CConnection унаследован от
public TLogHeader<'c', CConnection>

3.Который, используя CRTP, знает, что в CConnection есть такое:
	using this_t = CConnection;
	int m_id;           using m_id_t = TParamAccessor<this_t, decltype(this_t::m_id), &this_t::m_id>;
	std::string m_name; using m_name_t = TParamAccessor<this_t, decltype(this_t::m_name), &this_t::m_name>;
    char m_state;       using m_state_t = TParamAccessor<this_t, decltype(this_t::m_state), &this_t::m_state>;
	using log_header_param_list_t = std::tuple<m_id_t, m_name_t, m_state_t>;

4. И превращает эти данные в строчку лога:
c:23:test_conn 1:a:connection created

Где через двоеточие перечислены: с — первый параметр шаблона TLogHeader, и далее значения m_id, m_name, m_state

В моем случае все это дальше пишется в rsyslog и там разбирается в соответствующие поля для MongoDB (в зависимости от типа заголовка, т.е. первый параметр до двоеточия).

Сейчас этот подход у меня в продакшене на высоконагруженной системе, так что любые конструктивные камни будут в пику, или лучше под кат. А если лень, то вот работающий пример на http://coliru.stacked-crooked.com/ т.к. под катом кода на 250 строк, но очень много букв в описании.

Пардон, если будет много транслитерации и английских терминов. Статья полная отсебятина, но за 20 лет как-то от русской терминологии отстал. Поэтому не будет ссылок на литературу, хотя в одном моменте мне на stackoverflow.com помогли, но давать ссылку туда, значит палить свою анонимность как песочного юзера, насколько я понимаю.

Итак.

Часть 1 — Логгирование


Начнем с простого

TestLog::Log<LOG_NOTICE>() << *this << "connection created";


«template TestLog::Log» в нашем случае это тип (но можно так же реализовать как функцию, возвращающую тип). Как мы видим, создаётся временный объект этого типа. И, зная, что любой объект будет уничтожен при выходе из области видимости, понятно, что для данного типа это будет ';'. Соответственно, модель поведения простая — мы сливаем в этот тип данные, после чего вызывается деструктор.

Реализовано это в классе TSink
template<int logLevel, bool isNull>
struct TSink
{
    ~TSink()
    {
        std::cerr << "std::err log line flushed: \"" << stream.str() << " \"" << std::endl;
    }

    std::stringstream stream;
    
    template<typename T>
    TSink<logLevel, isNull>& operator << (T&& val)
    {
        stream << val;
        return *this;
    }
};

template<int logLevel>
struct TSink<logLevel, true>
{
    template<typename T>
    TSink<logLevel, true>& operator << (T&& val)
    {
        return *this;
    }
};

Частичная специализация тут нужна для того, чтобы «Иметь возможность выключать ненужные уровни логгирования на этапе компиляции одной константой; например: все что выше LOG_NOTICE не должно попасть в результирующий бинари». Это подарок нам от компилятора (в чем я не на 100% уверен, так как не сильно горю желанием декомпилировать код и смотреть что у нас на выходе; привет камни!). Идея простая: оператор записи в поток не производит никаких манипуляций с данными, и если принять во внимание, что у нас был создан временный объект и все что мы делаем до его удаления, это записываем в поток, то современный компилятор все это дело нам соптимизирует.

А вот так это будет выглядеть, если вместо cerr мы будем сливать в syslog
template<int logLevel, bool isNull>
struct TSink
{
    ~TSink()
    {
        syslog(logLevel, stream.str().c_str(), "junk");
    }
    
    std::stringstream stream;
    
    template<typename T>
    TSink<logLevel, isNull>& operator << (T&& val)
    {
        stream << val;
        return *this;
    }
};

template<int logLevel>
struct TSink<logLevel, true>
{
    template<typename T>
    TSink<logLevel, true>& operator << (T&& val)
    {
        return *this;
    }
};

Стоит упомянуть, что TSink, это уже конечная реализация, привязанная к конкретному модулю логгирования (например: стандартный поток вывода, файл, syslog, и проч.). Сам класс TLog, на который мы посмотрим позже, не знает как это будет реализовано, ему на вход поступают traits, которые уже и будут ссылаться на конкретную реализацию sink.

Посмотрим что такое TestLog::Log

Это:
static const int OUR_LOG_LEVEL = LOG_DEBUG;
using TestLog = TLog<OUR_LOG_LEVEL, TLogTraitsStdErr>; 

И, собственно, реализация TLog
template<int logUpTo, template<int> class log_traits>
struct TLog : public log_traits<logUpTo>
{
	using trats_t = log_traits<logUpTo>;
	
	template<int logLevel, bool isNull>
	using sink_type = typename trats_t::template sink_type<logLevel, isNull>;
	
	template<int neededLogLevel, int logLevel>
	struct TGetLogOfLevel
	{
		using type = typename std::conditional<
			neededLogLevel == logLevel,
			sink_type<neededLogLevel, (neededLogLevel > logUpTo)>,
			typename TGetLogOfLevel<neededLogLevel, logLevel + 1>::type
		>::type;
    };
	
	template<int neededLogLevel>
	struct TGetLogOfLevel<neededLogLevel, LOG_DEBUG + 1>
	{
        using type = void;
    };
	
	template<int neededLogLevel>
	using Log = typename TGetLogOfLevel<neededLogLevel, 0>::type;
};

На «вход шаблона» мы получаем два параметра: максимальный уровень логгирования в данной сборки программы (OUR_LOG_LEVEL) и шаблонный тип log_traits, от которого в дальнейшем наследуемся.

Как видно из кода к типу log_traits есть два требования:
  • тип должен принимать на вход int logUpTo (который форвардится из нашего первого параметра TLog)
  • тип должен иметь внутри определение другого шаблонного типа sink_type<int logLevel, bool isNull>, где logLevel — уровень текущей операции логгирования, и isNull — логгируем ли мы текущий уровень вообще (см. частичную специализацию TSink<logLevel, true>)

Внутри работает все очень просто: определяем тип Log который является результатом вызова рекурсивной мета-функции TGetLogOfLevel(neededLogLevel, logLevel), которая в случае neededLogLevel==logLevel возвращает sink_type текущего уровня, либо рекурсивно вызывает себя с уровнем логгирования +1.

На основании всего этого сделаем финальную реализацию log_traits

Для этого обернем наш готовый TSink в новый класс TLogTraits учитывая требования к log_traits, описанные выше. Т.е. определим тип traits_type как
template<int logLevel, bool isNull>
using sink_type = TSink<logLevel, isNull>;
Также добавим функцию OpenLog и в деструкторе его (лог) закроем.

Вот код TLogTraitsStdErr
template<int logUpTo>
struct TLogTraitsStdErr
{
    void Open(const char* logName, int facility)
	{
    	std::cerr << "std::err log opened with logName: " << logName << std::endl;
	}
	
    ~TLogTraitsStdErr()
	{
        std::cerr << "std::err log closed" << std::endl;
	}
	
    template<int logLevel, bool isNull>
    struct TSink
    {
        ~TSink()
        {
            std::cerr << "std::err log line flushed: \"" << stream.str() << " \"" << std::endl;
        }
    
        std::stringstream stream;
        
        template<typename T>
        TSink<logLevel, isNull>& operator << (T&& val)
        {
            stream << val;
            return *this;
        }
    };

    template<int logLevel>
    struct TSink<logLevel, true>
    {
        template<typename T>
        TSink<logLevel, true>& operator << (T&& val)
        {
            return *this;
        }
    };
	
	template<int logLevel, bool isNull>
	using sink_type = TSink<logLevel, isNull>;
};

И по аналогии TLogTraitsSyslog
template<int logUpTo>
struct TLogTraitsSyslog
{
	static void Open(const char* logName, int facility)
	{
		openlog(logName, LOG_PID | LOG_NDELAY, facility);
		setlogmask(LOG_UPTO(logUpTo));
	}
	
	~TLogTraitsSyslog()
	{
		closelog();
	}
	
    template<int logLevel, bool isNull>
    struct TSink
    {
        ~TSink()
        {
            syslog(logLevel, stream.str().c_str(), "junk");
        }
        
        std::stringstream stream;
        
        template<typename T>
        TSink<logLevel, isNull>& operator << (T&& val)
        {
            stream << val;
            return *this;
        }
    };
    
    template<int logLevel>
    struct TSink<logLevel, true>
    {
        template<typename T>
        TSink<logLevel, true>& operator << (T&& val)
        {
            return *this;
        }
    };
	
	template<int logLevel, bool isNull>
	using sink_type = TSink<logLevel, isNull>;
};

И, конечно же, потестируем эту котовасию

Напишем testcase
static const int OUR_LOG_LEVEL 
    = LOG_DEBUG;

using TestLog = TLog<OUR_LOG_LEVEL, TLogTraitsStdErr>;

struct CConnection
{
	CConnection()
    {
        TestLog::Log<LOG_NOTICE>() << "connection created";
    }
 
    ~CConnection()
    {
        TestLog::Log<LOG_NOTICE>() << "connection destroyed";
    }
    
    void DoSomething()
    {
        TestLog::Log<LOG_DEBUG>() << "connection is doing something";
    }
};

class CServer : public TestLog
{
public:

    CServer()
	{
		TestLog::Open("test_log", 1);
	};

    int Run()
    {
        TestLog::Log<LOG_DEBUG>() << "Creating connection";
        
        CConnection test_conn1;
        test_conn1.DoSomething();
        
        return 0;
    }
};
    
int main(int argc, char** argv)
{
    CServer server;
    
    return server.Run();
}

Скомпилируем и получим вот такой output:
clang++ -std=c++11 -O2 -Wall -pedantic  -pthread main.cpp  && ./a.out
std::err log opened with logName: test_log
std::err log line flushed: "Creating connection "
std::err log line flushed: "connection created "
std::err log line flushed: "connection is doing something "
std::err log line flushed: "connection destroyed "
std::err log closed

И поиграем с ним на (даже не знаю, как этот ресурс назвать) Coliru

В результате у нас получилось 25 строк мета-класс TLog, и по ~40 на каждую конкретную реализацию log_traits. И требования выполнены: констатнту выключающую ненужное логгирование сделали; просто? — вроде да; прозрачно? — если читать мета код не составляет сложности.
KISS с моей точки зрения.
А как вам? Привет камни 2!

Часть 2 — Рефлекшен


Расширим CConnection в нашем testcase

Помните, в начале были интересные плюшки:
TLogHeader<'c', CConnection>, используя CRTP, знает, что в CConnection есть такое:
	using this_t = CConnection;
	int m_id;           using m_id_t = TParamAccessor<this_t, decltype(this_t::m_id), &this_t::m_id>;
	std::string m_name; using m_name_t = TParamAccessor<this_t, decltype(this_t::m_name), &this_t::m_name>;
    char m_state;       using m_state_t = TParamAccessor<this_t, decltype(this_t::m_state), &this_t::m_state>;
	using log_header_param_list_t = std::tuple<m_id_t, m_name_t, m_state_t>;

Вот наш расширенный CConnection
struct CConnection : public TLogHeader<'c', CConnection>
{
	CConnection(int _id, const std::string& _name) : m_id(_id), m_name(_name), m_state('a')
    {
        TestLog::Log<LOG_NOTICE>() << *this << "connection created";
        m_state = 'b';
    }
 
    ~CConnection()
    {
        TestLog::Log<LOG_NOTICE>() << *this << "connection destroyed";
    }
    
    void DoSomething()
    {
        TestLog::Log<LOG_DEBUG>() << *this << "connection is doing something";
        m_state = 'c';
    }
    
	using this_t = CConnection;

	int m_id;           using m_id_t = TParamAccessor<this_t, decltype(this_t::m_id), &this_t::m_id>;
	std::string m_name; using m_name_t = TParamAccessor<this_t, decltype(this_t::m_name), &this_t::m_name>;
    char m_state;       using m_state_t = TParamAccessor<this_t, decltype(this_t::m_state), &this_t::m_state>;
	
	using log_header_param_list_t = std::tuple<m_id_t, m_name_t, m_state_t>;
};

Конечно, все это можно обернуть в простой макрос, типа PARAM(int, m_id), но не будем for the sanity sake.

TParamAccessor — это тип в котором определена одна статическая функция GetRef, возвращающая ссылку на переменную класса.
Вот так вот
struct TParamAccessorDefaultTraits
{
};

template <typename _object_t, typename _value_type, _value_type _object_t::*member_t, typename _traits_t = TParamAccessorDefaultTraits>
struct TParamAccessor : public _traits_t
{
    using traits_t = _traits_t;
    using object_t = _object_t;
    using value_type = _value_type;
		
	static value_type& GetRef(object_t& data) { return data.*member_t; }
};
Соответственно, для того, чтобы нам запихнуть *this в поток sink_type, нам нужно, унаследовавшись от TLogHeader<'c', CConnection> проитерироваться внури него по CConnection::log_header_param_list_t и вызвать sink_type << TParamAccessor::GetRef(наш объект).

Warning: тут будет немного сложно для восприятия, пока не посмотрим на мета вариант for_each

Мы получаем класс TLogHeader
template<char moduleName, typename object_t>
struct TLogHeader
{
	template<size_t idx, typename accessor_t>
	struct TLogHeaderWriter
	{
		using type = TLogHeaderWriter<idx, accessor_t>;

		static void call(typename accessor_t::object_t& obj, std::stringstream& out)
		{
			typename accessor_t::value_type& val = accessor_t::GetRef(obj);
			out << val << ":";
		}
	};
	
	template<typename sink_type>
	friend sink_type& operator << (sink_type& sink, object_t& val)
	{
		std::stringstream header;
		
		using writers = typename for_each<typename object_t::log_header_param_list_t>::template instantiate<TLogHeaderWriter>;
		for_each<writers>::call(*static_cast<object_t*>(&val), header);

		sink << moduleName << ":" << header.str();
		return sink;
	}
};

TLogHeader описывает только одну функцию — перегруженный оператор записи в поток.
В которой мы видим for_each в двух ипостасях:
  1. вызов мета-функции для определения типа writers, в который возвращаются в виде tuple все инстансы TLogHeaderWriter<от object_t::log_header_param_list_t:: от каждого типа внутри>)
  2. вызов статической функции call для всех типов определенных в writers

Сделаем «мета-ипостась» for_each

Для этого упростим наш TLogHeader до вызова только мета-функции
template<char moduleName, typename object_t>
struct TLogHeader
{
	template<size_t idx, typename accessor_t>
	struct TLogHeaderWriter
	{
		using type = TLogHeaderWriter<idx, accessor_t>;
	};
	
	template<typename sink_type>
	friend sink_type& operator << (sink_type& sink, object_t& val)
	{
		using writers = typename for_each<typename object_t::log_header_param_list_t>::template instantiate<TLogHeaderWriter>;

		return sink;
	}
};

И напишем мета функцию.
Disclaimer: т.к. для простоты урезал финальный код, не тестировал; финальный можно будет позже потестить в живую.

На всякий случай уточню: в наиболее приятной мне терминологии, «мета-функция, это функция выполняемая на этапе компиляции программы, и результатом которой является новый тип» (все своими словами)

Прокомментирую в коде, т.к. далее немного запутано пойдет.

Код for_each
////////////////////////////////////////////////////////////////
// append to tuple

//мета-функция добавления к кортежу еще одного типа new_t
//можно вызывать как для tuple так и для template argument pack, как видно по использованию частичной специализации

template<typename new_t, typename... other_t>
struct append_to_tuple
{
	using type = std::tuple<other_t..., new_t>;
};

template<typename new_t, typename... other_t>
struct append_to_tuple<new_t, std::tuple<other_t...>>
{
	using type = std::tuple<other_t..., new_t>;
};

////////////////////////////////////////////////////////////////
// for_each_impl

//реализация for_each - рекурсивная метафункция, инстантиирующая объект func_t методом instatiate,
//который, в свою очередь дописывает новый тип в конец tuple методом append_to_tuple
//вынесена отдельно, для того, чтобы пробросить индекс и не указывать его в конечной реализации

template<size_t i, typename... args_t>
struct for_each_impl
{
	using this_type = typename std::tuple_element<i, std::tuple<args_t...>>::type;
	using prev_type = for_each_impl<i - 1, args_t...>;

	template<template<size_t, typename> class func_t>
	using instantiate = 
		typename append_to_tuple<
			typename func_t<i, this_type>::type, 
			typename prev_type::template instantiate<func_t>
		>::type;
};

// идем от последнего аргумента к первому
// данная частичная специализация является остановкой рекурсии

template<typename... args_t>
struct for_each_impl<0, args_t...>
{
	using this_type = typename std::tuple_element<0, std::tuple<args_t...>>::type;
	
	template<template<size_t, typename> class func_t>
	using instantiate = std::tuple<typename func_t<0, this_type>::type>;
};

///////////////////////////////////////////////////////////////
// for_each

//конечная реализация.
//ссылается на for_each_impl, пробрасывая количество аргументов шаблона
// + есть две специализации для tuple<с параметрами> и tuple<> без оных 

template<typename... args_t>
struct for_each
{
	using prev_type = for_each_impl<sizeof...(args_t) - 1, args_t...>;
	
	template<template<size_t, typename> class func_t>
	using instantiate = typename prev_type::template instantiate<func_t>;
};

template<typename... args_t>
struct for_each<std::tuple<args_t...>> : public for_each<args_t...>
{
};

template<>
struct for_each<std::tuple<>>
{
	template<template<size_t, typename> class func_t>
	using instantiate = std::tuple<>;
};

Таким образом, мы получили using writers = typename for_each<typename object_t::log_header_param_list_t>::template instantiate; где writers развернется в:
std::tuple<
	TLogHeaderWriter<0, TParamAccessor<CConnection, decltype(CConnection::m_id), &CConnection::m_id>>,
	TLogHeaderWriter<1, TParamAccessor<CConnection, decltype(CConnection::m_name), &CConnection::m_name>>,
	TLogHeaderWriter<2, TParamAccessor<CConnection, decltype(CConnection::m_state), &CConnection::m_state>>
>;

Осталось пройтись по этим типам, и вызвать GetRef c последующей записью в поток.

Сделаем «ран-тайм-ипостась» for_each

И сразу посмотрим на расширенный for_each, в который добавлены вызовы статической функции call (только на нее и стоит обращать внимание, остальное все без изменений)
И вот он - последний кусок кода
////////////////////////////////////////////////////////////////
// for_each_impl

template<size_t i, typename... args_t>
struct for_each_impl
{
	using this_type = typename std::tuple_element<i, std::tuple<args_t...>>::type;
	using prev_type = for_each_impl<i - 1, args_t...>;

	template<template<size_t, typename> class func_t>
	using instantiate = 
		typename append_to_tuple<
			typename func_t<i, this_type>::type, 
			typename prev_type::template instantiate<func_t>
		>::type;
	
	template<typename object_t, typename... call_args_t>
	static void call(object_t&& obj, call_args_t&&... args)
	{
		prev_type::call(std::forward<object_t>(obj), std::forward<call_args_t>(args)...);
		this_type::call(std::forward<object_t>(obj), std::forward<call_args_t>(args)...);
	}
};

template<typename... args_t>
struct for_each_impl<0, args_t...>
{
	using this_type = typename std::tuple_element<0, std::tuple<args_t...>>::type;
	
	template<template<size_t, typename> class func_t>
	using instantiate = std::tuple<typename func_t<0, this_type>::type>;
	
	template<typename object_t, typename... call_args_t>
	static void call(object_t&& obj, call_args_t&&... args)
	{
		this_type::call(std::forward<object_t>(obj), std::forward<call_args_t>(args)...);
	}
	
	template<typename object_t>
	static void call(object_t&& obj)
	{
		this_type::call(std::forward<object_t>(obj));
	}
};

///////////////////////////////////////////////////////////////
// for_each

template<typename... args_t>
struct for_each
{
	using prev_type = for_each_impl<sizeof...(args_t) - 1, args_t...>;
	
	template<template<size_t, typename> class func_t>
	using instantiate = typename prev_type::template instantiate<func_t>;

	template<typename object_t, typename... call_args_t>
	static object_t call(object_t&& obj, call_args_t&&... args)
	{
		prev_type::call(std::forward<object_t>(obj), std::forward<call_args_t>(args)...);
		return obj;
	}
	
	template<typename object_t>
	static object_t call(object_t&& obj)
	{
		prev_type::call(std::forward<object_t>(obj));
		return obj;
	}
};

template<typename... args_t>
struct for_each<std::tuple<args_t...>> : public for_each<args_t...>
{
};

template<>
struct for_each<std::tuple<>>
{
	template<template<size_t, typename> class func_t>
	using instantiate = std::tuple<>;

	template<typename object_t, typename... call_args_t>
	static object_t call(object_t&& obj, call_args_t&&... args)
	{
		return obj;
	}
	
	template<typename object_t>
	static object_t call(object_t&& obj)
	{
		return obj;
	}
};

Думаю, тут комментировать особо не надо: call получает на вход как минимум один параметр, который является нашим прокси объектом, и который по окончанию работы возвращается — остальные параметры форвардятся в вызовы call.
В нашем случае — это TLogHeaderWirter::call:
static void call(typename accessor_t::object_t& obj, std::stringstream& out)
{
    typename accessor_t::value_type& val = accessor_t::GetRef(obj);
    out << val << ":";
}
Где accessor_t::object_t = CConnection
Ну и, конечно, perfect forwarding — вещь!

Часть 3 — Заключение


Весь код целиком
#include <cstdlib>
#include <iostream>
#include <sstream>

#include <syslog.h>

static const int OUR_LOG_LEVEL 
    = LOG_DEBUG;
    // = LOG_NOTICE;

// log up to LOG_DEBUG output:
//std::err log opened with logName: test_log
//std::err log line flushed: "s:1:test server:Creating connection "
//std::err log line flushed: "c:23:test_conn 1:a:connection created "
//std::err log line flushed: "c:23:test_conn 1:b:connection is doing something "
//std::err log line flushed: "c:23:test_conn 1:c:connection destroyed "
//std::err log closed

// log up to LOG_NOTICE output:
//std::err log opened with logName: test_log
//std::err log line flushed: "c:23:test_conn 1:a:connection created "
//std::err log line flushed: "c:23:test_conn 1:c:connection destroyed "
//std::err log closed

// ---------------------------- for_each.h start ----------------------------//
#include <tuple>
#include <utility>

namespace helpers
{

////////////////////////////////////////////////////////////////
// param accessor

struct TParamAccessorDefaultTraits
{
};

template <typename _object_t, typename _value_type, _value_type _object_t::*member_t, typename _traits_t = TParamAccessorDefaultTraits>
struct TParamAccessor : public _traits_t
{
    using traits_t = _traits_t;
    using object_t = _object_t;
    using value_type = _value_type;
		
	static value_type& GetRef(object_t& data) { return data.*member_t; }
};

////////////////////////////////////////////////////////////////
// append to tuple

template<typename new_t, typename... other_t>
struct append_to_tuple
{
	using type = std::tuple<other_t..., new_t>;
};

template<typename new_t, typename... other_t>
struct append_to_tuple<new_t, std::tuple<other_t...>>
{
	using type = std::tuple<other_t..., new_t>;
};

////////////////////////////////////////////////////////////////
// for_each_impl

template<size_t i, typename... args_t>
struct for_each_impl
{
	using this_type = typename std::tuple_element<i, std::tuple<args_t...>>::type;
	using prev_type = for_each_impl<i - 1, args_t...>;

	template<template<size_t, typename> class func_t>
	using instantiate = 
		typename append_to_tuple<
			typename func_t<i, this_type>::type, 
			typename prev_type::template instantiate<func_t>
		>::type;
	
	template<typename object_t, typename... call_args_t>
	static void call(object_t&& obj, call_args_t&&... args)
	{
		prev_type::call(std::forward<object_t>(obj), std::forward<call_args_t>(args)...);
		this_type::call(std::forward<object_t>(obj), std::forward<call_args_t>(args)...);
	}
};

template<typename... args_t>
struct for_each_impl<0, args_t...>
{
	using this_type = typename std::tuple_element<0, std::tuple<args_t...>>::type;
	
	template<template<size_t, typename> class func_t>
	using instantiate = std::tuple<typename func_t<0, this_type>::type>;
	
	template<typename object_t, typename... call_args_t>
	static void call(object_t&& obj, call_args_t&&... args)
	{
		this_type::call(std::forward<object_t>(obj), std::forward<call_args_t>(args)...);
	}
	
	template<typename object_t>
	static void call(object_t&& obj)
	{
		this_type::call(std::forward<object_t>(obj));
	}
};

///////////////////////////////////////////////////////////////
// for_each

template<typename... args_t>
struct for_each
{
	using prev_type = for_each_impl<sizeof...(args_t) - 1, args_t...>;
	
	template<template<size_t, typename> class func_t>
	using instantiate = typename prev_type::template instantiate<func_t>;

	template<typename object_t, typename... call_args_t>
	static object_t call(object_t&& obj, call_args_t&&... args)
	{
		prev_type::call(std::forward<object_t>(obj), std::forward<call_args_t>(args)...);
		return obj;
	}
	
	template<typename object_t>
	static object_t call(object_t&& obj)
	{
		prev_type::call(std::forward<object_t>(obj));
		return obj;
	}
};

template<typename... args_t>
struct for_each<std::tuple<args_t...>> : public for_each<args_t...>
{
};

template<>
struct for_each<std::tuple<>>
{
	template<template<size_t, typename> class func_t>
	using instantiate = std::tuple<>;

	template<typename object_t, typename... call_args_t>
	static object_t call(object_t&& obj, call_args_t&&... args)
	{
		return obj;
	}
	
	template<typename object_t>
	static object_t call(object_t&& obj)
	{
		return obj;
	}
};

} //namespace helpers

// ---------------------------- for_each.h end ----------------------------//


using namespace helpers;

//////////////////////////////////////////////////////////////////////////////////////////


template<int logUpTo>
struct TLogTraitsStdErr
{
    static void Open(const char* logName, int facility)
	{
    	std::cerr << "std::err log opened with logName: " << logName << std::endl;
	}
	
    ~TLogTraitsStdErr()
	{
        std::cerr << "std::err log closed" << std::endl;
	}
	
	template<int logLevel, bool isNull>
	struct TSink
	{
		~TSink()
        {
            std::cerr << "std::err log line flushed: \"" << stream.str() << " \"" << std::endl;
        }

		std::stringstream stream;

		template<typename T>
		TSink<logLevel, isNull>& operator << (T&& val)
		{
			stream << val;
			return *this;
		}
	};

	template<int logLevel>
	struct TSink<logLevel, true>
	{
		template<typename T>
		TSink<logLevel, true>& operator << (T&& val)
		{
			return *this;
		}
	};
	
	template<int logLevel, bool isNull>
	using sink_type = TSink<logLevel, isNull>;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

template<int logUpTo>
struct TLogTraitsSyslog
{
	static void Open(const char* logName, int facility)
	{
		openlog(logName, LOG_PID | LOG_NDELAY, facility);
		setlogmask(LOG_UPTO(logUpTo));
	}
	
	~TLogTraitsSyslog()
	{
		closelog();
	}
	
    template<int logLevel, bool isNull>
    struct TSink
    {
        ~TSink()
        {
            syslog(logLevel, stream.str().c_str(), "junk");
        }
        
        std::stringstream stream;
        
        template<typename T>
        TSink<logLevel, isNull>& operator << (T&& val)
        {
            stream << val;
            return *this;
        }
    };
    
    template<int logLevel>
    struct TSink<logLevel, true>
    {
        template<typename T>
        TSink<logLevel, true>& operator << (T&& val)
        {
            return *this;
        }
    };
	
	template<int logLevel, bool isNull>
	using sink_type = TSink<logLevel, isNull>;
};


//////////////////////////////////////////////////////////////////////////////////////////

template<char moduleName, typename object_t>
struct TLogHeader
{
	template<size_t idx, typename accessor_t>
	struct TLogHeaderWriter
	{
		using type = TLogHeaderWriter<idx, accessor_t>;

		static void call(typename accessor_t::object_t& obj, std::stringstream& out)
		{
			typename accessor_t::value_type& val = accessor_t::GetRef(obj);
			out << val << ":";
		}
	};
	
	template<typename sink_type>
	friend sink_type& operator << (sink_type& sink, object_t& val)
	{
		std::stringstream header;
		
		using writers = typename for_each<typename object_t::log_header_param_list_t>::template instantiate<TLogHeaderWriter>;
		for_each<writers>::call(*static_cast<object_t*>(&val), header);

		sink << moduleName << ":" << header.str();
		return sink;
	}
};

//////////////////////////////////////////////////////////////////////////////////////////
	
template<int logUpTo, template<int> class log_traits = TLogTraitsSyslog>
struct TLog : public log_traits<logUpTo>
{
    using trats_t = log_traits<logUpTo>;
    
    template<int logLevel, bool isNull>
    using sink_type = typename trats_t::template sink_type<logLevel, isNull>;
    
    template<int neededLogLevel, int logLevel>
    struct TGetLogOfLevel
    {
        using type = typename std::conditional<
            neededLogLevel == logLevel,
            sink_type<neededLogLevel, (neededLogLevel > logUpTo)>,
            typename TGetLogOfLevel<neededLogLevel, logLevel + 1>::type
        >::type;
     };
    
    template<int neededLogLevel>
    struct TGetLogOfLevel<neededLogLevel, LOG_DEBUG + 1>
    {
        using type = void;
    };
    
    template<int neededLogLevel>
    using Log = typename TGetLogOfLevel<neededLogLevel, 0>::type;
};

///////////////////////////////
//testcase

using TestLog = TLog<OUR_LOG_LEVEL, TLogTraitsStdErr>;

struct CConnection : public TLogHeader<'c', CConnection>
{
	CConnection(int _id, const std::string& _name) : m_id(_id), m_name(_name), m_state('a')
    {
        TestLog::Log<LOG_NOTICE>() << *this << "connection created";
        m_state = 'b';
    }
 
    ~CConnection()
    {
        TestLog::Log<LOG_NOTICE>() << *this << "connection destroyed";
    }
    
    void DoSomething()
    {
        TestLog::Log<LOG_DEBUG>() << *this << "connection is doing something";
        m_state = 'c';
    }
    
	using this_t = CConnection;

	int m_id;           using m_id_t = TParamAccessor<this_t, decltype(this_t::m_id), &this_t::m_id>;
	std::string m_name; using m_name_t = TParamAccessor<this_t, decltype(this_t::m_name), &this_t::m_name>;
    char m_state;       using m_state_t = TParamAccessor<this_t, decltype(this_t::m_state), &this_t::m_state>;
	
	using log_header_param_list_t = std::tuple<m_id_t, m_name_t, m_state_t>;
};

class CServer :  public TLogHeader<'s', CServer>, public TestLog
{
public:

    CServer() : m_id(1), m_name("test server")
	{
		TestLog::Open("test_log", 1);
	};

    int Run()
    {
        TestLog::Log<LOG_DEBUG>() << *this << "Creating connection";
        
        CConnection test_conn1(23, "test_conn 1");
        test_conn1.DoSomething();
        
        return 0;
    }
    
    using this_t = CServer;

	int m_id; using m_id_t = TParamAccessor<this_t, decltype(this_t::m_id), &this_t::m_id>;
	std::string m_name; using m_name_t = TParamAccessor<this_t, decltype(this_t::m_name), &this_t::m_name>;
	
	using log_header_param_list_t = std::tuple<m_id_t, m_name_t> ;
};
    
int main(int argc, char** argv)
{
    CServer server;
    
    return server.Run();
}

Вот готовый пример.
В него мы добавили еще 100 строчек имплементации 2х вариантов for_each.
И, если вы заглянули в пример, можно увидеть что они в отдельном файле for_each.h
Т.к. я его активно использую уже в 5ти модулях моей истории, таких как: нативный бинарный формат общения с постгресом учитывая endianess конечных систем, разворачивание пакетов из буферов и генерацию формата сообщений по вебсокетам на этапе компиляции, и т.д.

И как уже говорил: Камни! Приём!

P.S. Написание кода заняло пол дня, а статья на хабр день… парадокс!
Tags:
Hubs:
0
Comments 3
Comments Comments 3

Articles