Добавил трассировку механизма доставки сообщений до агентов в версию 5.5.9. По ходу дела пришлось решить одну задачку, которая может быть интересна сама по себе, безотносительно SObjectizer. А именно: как создать две слегка отличающися версии одного кода, но без его дублирования и без потери его эффективности.
Грубо говоря, вот одно из мест, которое изначально выглядело приблизительно вот так:
void local_mbox_t::do_deliver_message( ... ) const { read_lock_guard_t< default_rw_spinlock_t > lock( m_lock ); auto it = m_subscribers.find( msg_type ); if( it != m_subscribers.end() ) for( auto a : it->second ) { if( a.must_be_delivered( *(message.get()) ) ) try_to_deliver_to_agent< invocation_type_t::event >( ..., [&] { agent_t::call_push_event( ...); } ); } } |
Нужно было сделать так, чтобы в случае, когда трассировка выключена, код оставался таким же. А в случае, когда трассировка включена, в нем появилось несколько дополнительных строчек (выделены жирным):
void local_mbox_t::do_deliver_message( ... ) const { read_lock_guard_t< default_rw_spinlock_t > lock( m_lock ); auto it = m_subscribers.find( msg_type ); if( it != m_subscribers.end() ) for( auto a : it->second ) { if( a.must_be_delivered( *(message.get()) ) ) try_to_deliver_to_agent< invocation_type_t::event >( ..., [&] { m_trace.push_to_queue(...); agent_t::call_push_event( ... ); } ); else m_trace.message_rejected(...); } else m_trace.no_subscribers(...); } |
Причем такая трансформация должна была происходить в run-time, а не в compile-time, т.е. результирующая бинарная DLL должна была содержать обе версии кода.
По какому пути стоило пойти?
Можно было бы воспользоваться самым тупым вариантом: копирование кода с последующей модификацией одной из копий. Но это означало бы, что ключевые фрагменты механизмов распределения сообщений по подписчикам были бы продублированы. Со всеми вытекающими отсюда последствиями вроде внесения одинаковых правок в разные места при сопровождении и необходимость тестирования каждой правки отдельно.
Поэтому самый тупой вариант даже не рассматривался. Более привлекательно выглядел вариант с элементарным if-ом. Т.е. в местах, где требуется трассировка, ставится if:
void local_mbox_t::do_deliver_message( ... ) const { read_lock_guard_t< default_rw_spinlock_t > lock( m_lock ); auto it = m_subscribers.find( msg_type ); if( it != m_subscribers.end() ) for( auto a : it->second ) { if( a.must_be_delivered( *(message.get()) ) ) try_to_deliver_to_agent< invocation_type_t::event >( ..., [&] { if( m_tracer ) m_tracer->push_to_queue(...); agent_t::call_push_event( ... ); } ); else if( m_tracer ) m_tracer->message_rejected(...); } else if( m_tracer ) m_tracer->no_subscribers(...); } |
Очень соблазнительный вариант и в одном из мест что-то подобное и было сделано.
Однако, при таком подходе ситуация с выключенной трассировкой оказывается небесплатной. Помимо if-ов, в mbox-ах придется хранить еще и указатель на объект-tracer, даже если этот указатель будет нулевым. Итого, в самом распространном сценарии с отключенной трассировкой придется платить как увеличившимся в размере mbox-ом, так и ненужными if-ами в наиболее часто используемых операциях. Плата, конечно, небольшая, но если ее можно не платить, то лучше не платить.
Можно было бы покурить какую-нибудь тему на макросах. Т.е. внутри хитрого макроса один раз пишется код, который затем посредством #define и #ifdef-ов преобразуется либо в один вариант, либо во второй.
Но связываться с макросами не хотелось. Слишком уж мало контроля за ними, да и в случае каких-нибудь ошибок разбираться в дебрях макросов сложно.
Поэтому воспользовался небольшой шаблонной магией.
Суть в том, чтобы определить две вспомогательные структуры с однаковым набором методов. Только одна структура пустая, а вторая -- нет. Что-то вроде:
struct tracing_disabled_base_t { void no_subscribers( /*params*/ ) const {} void push_to_queue( /*params*/ ) const {} void message_rejected( /*params*/ ) const {} }; class tracing_enabled_base_t { tracer_t & m_tracer; public : tracing_enabled_base_t( tracer_t & tracer ) : m_tracer{tracer} {} void no_subscribers( /*params*/ ) const { ... /* some code */ } void push_to_queue( /*params*/ ) const { ... /* some code */ } void message_rejected( /*params*/ ) const { ... /* some code */ } }; |
После чего классы mbox-ов были преобразованы в шаблоны, параметром которого был либо тип tracing_enabled_base_t, либо tracing_disabled_base_t. Что-то вроде:
template< typename TRACING_BASE > class local_mbox_template_t : public abstract_message_box_t , private local_mbox_details::data_t , private TRACING_BASE // <-- The most important part! { ... }; |
Ну а при создании соответствующего mbox-а просто проверялось, включена ли трассировка или нет, и создавался объект соответствующего типа. Приблизительно вот так:
mbox_t create_local_mbox() { auto id = ++m_mbox_id_counter; if( !m_tracer ) return mbox_t{ new local_mbox_template_t< tracing_disabled_base_t >{ id } }; else return mbox_t{ new local_mbox_template_t< tracing_enabled_base_t >{ id, *m_tracer } }; } |
Получилось, что код, подлежащий трассировке, собран в одном шаблонном классе local_mbox_template_t. И выглядеть этот код стал приблизительно таким образом:
void local_mbox_t::do_deliver_message( ... ) const { read_lock_guard_t< default_rw_spinlock_t > lock( m_lock ); auto it = m_subscribers.find( msg_type ); if( it != m_subscribers.end() ) for( auto a : it->second ) { if( a.must_be_delivered( *(message.get()) ) ) try_to_deliver_to_agent< invocation_type_t::event >( ..., [&] { this->push_to_queue(...); agent_t::call_push_event( ... ); } ); else this->message_rejected(...); } else this->no_subscribers(...); } |
Фокус здесь в том, что методы push_to_queue(), message_rejected() и no_subscribers() класс local_mbox_template_t наледует из своего базового класса (который, напомню, задается параметром шаблона). Соответственно, когда local_mbox_template_t наследуется от tracing_disabled_base_t, то эти методы пусты и их вызовы выбрасываются компилятором. Если же local_mbox_template_t наследуется от tracing_enabled_base_t, то он получает от своего базового класса не только полную реализацию этих методов, но и атрибут m_tracer.
Вот таким образом, посредством наследования от параметра шаблона, из одного кода получается две отличающиеся друг от друга реализации. Отличающиеся как по составу полей, так и по деталям своей работы. И без каких-либо макросов, с полным контролем со стороны компилятора.
Причем, что важно, методы push_to_queue() со товарищи, не виртуальные. Их вызовы внутри local_mbox_template_t разруливаются в compile-time, что гораздо лучше, чем если бы я попытался воспользоваться чистым ООП с виртуальными методами.
Да! Тут же еще есть одна маленькая, но важная деталька ;)
Легко увидеть, что конструктор local_mbox_template_t<T> в зависимости от параметра шаблона имеет разное количество аргументов в конструкторе. Достигается это за счет того, что local_mbox_template_t имеет шаблонный конструктор, да не простой, а с переменным количеством параметров ;)
template< typename TRACING_BASE > class local_mbox_template_t : public abstract_message_box_t , private local_mbox_details::data_t , private TRACING_BASE { public: template< typename... TRACING_ARGS > local_mbox_template_t( //! ID of this mbox. mbox_id_t id, //! Optional parameters for TRACING_BASE's constructor. TRACING_ARGS &&... args ) : local_mbox_details::data_t{ id } , TRACING_BASE{ std::forward< TRACING_ARGS >(args)... } {} |
А поскольку переменное количество параметров может быть и нулевым, то это и дает возможность конструктору local_mbox_template_t<tracing_disabled_base_t> иметь другой набор аргументов, чем конструктор local_mbox_template_t<tracing_enabled_base_t>
Вот такие интересные возможности дает разработчику C++11. На таких примерах хорошо видно, что C++11 -- это уже совсем другой язык, после которого на C++98/03 возвращаться не хочется.
Завтра постараюсь написать вторую часть, где покажу, как возможности C++11 снизили объем дублирующего кода при формировании теста трейсов. Upd. См.продолжение.
PS. В данном посте показа принципиальная схема задействованной в коде SO-5.5.9 идеи. Реальная реализация несколько отличается и далеко не везде удалось получить вариант с полностью нулевыми накладными расходами при отключенном трейсинге. Поэтому актуальная реализация несколько отличается от показанной выше, но не принципиально.
Комментариев нет:
Отправить комментарий