четверг, 22 октября 2015 г.

[prog.c++11] Опыт добавления трассировки в SO-5.5.9. Часть II. Variadic templates для упрощения рутины и уменьшения дублирования

Еще один очень простой пример использования возможностей современного C++ для сокращения объема "тупого" кода. На этот раз variadic templates были задействованы для того, чтобы упростить формирование похожих друг на друга строчек для последующего логирования.

Первая реализация механизма трассировки процесса доставки сообщений до агентов была написана "в лоб" и содержало большое количество вот таких фрагментов:

class deliver_op_tracer_t
   {
   ...
   public :
      ...
      void
      no_subscribers() const
         {
            std::ostringstream s;

            s << "msg_trace [tid=" << query_current_thread_id()
               << "][mbox_id=" << m_mbox.id()
               << "][mbox_name=" << m_mbox.query_name()
               << "] " << m_op_name << ".no_subscribers "
               << "[msg_type=" << m_msg_type.name()
               << "][msg_ptr=" << m_message.get()
               << "][overlimit_deep=" << m_overlimit_reaction_deep
               << "]";

            m_tracer.trace( s.str() );
         }

      void
      push_to_queue( const agent_t * subscriber ) const
         {
            std::ostringstream s;

            s << "msg_trace [tid=" << query_current_thread_id()
               << "][mbox_id=" << m_mbox.id()
               << "][mbox_name=" << m_mbox.query_name()
               << "] " << m_op_name << ".push_to_queue "
               << "[msg_type=" << m_msg_type.name()
               << "][msg_ptr=" << m_message.get()
               << "][overlimit_deep=" << m_overlimit_reaction_deep
               << "][agent_ptr=" << subscriber << "]";

            m_tracer.trace( s.str() );
         }
      ...
   };

Очевидно, что это не есть хорошо. Слишком большой объем кода, слишком легко допустить ошибку, слишком сложно вносить изменения. Кроме того, этот код нельзя было оставлять в таком виде, т.к. методы класса deliver_op_tracer_t не должны выпускать наружу исключений.

Поэтому как только представилась возможность, то была проведена следующая итерация и код был приведен вот к такому виду:

class deliver_op_tracer_t
   {
   ...
   public :
      ...
      void
      no_subscribers() const
         {
            details::make_trace(
                  m_tracer,
                  m_mbox,
                  details::composed_action_name_t{
                        m_op_name, "no_subscribers" },
                  m_msg_type,
                  m_message,
                  m_overlimit_deep );
         }

      void
      push_to_queue( const agent_t * subscriber ) const
         {
            details::make_trace(
                  m_tracer,
                  m_mbox,
                  details::composed_action_name_t{
                        m_op_name, "push_to_queue" },
                  m_msg_type,
                  m_message,
                  m_overlimit_deep,
                  subscriber );
         }
      ...
   };

Вспомогательная функция make_trace, определенная в пространстве имен details, написана с использованием variadic templates. Но об этой функции чуть позже.

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

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

class deliver_op_tracer_t
   {
   ...
   public :
      ...
      void
      no_subscribers() const
         {
            make_trace( "no_subscribers" );
         }

      void
      push_to_queue( const agent_t * subscriber ) const
         {
            make_trace( "push_to_queue", subscriber );
         }
   };

Разница ошутима, не так ли? ;)

Все дело в очень простом новом методе deliver_op_tracer_t::make_trace(), который, сюрпрайз-сюрпрайз, так же написан с использованием variadic templates:

class deliver_op_tracer_t
   {
   ...
      templatetypename... ARGS >
      void
      make_trace(
         const char * action_name_suffix,
         ARGS &&... args ) const
         {
            details::make_trace(
                  m_tracer,
                  m_mbox,
                  details::composed_action_name_t{
                        m_op_name, action_name_suffix },
                  m_msg_type,
                  m_message,
                  m_overlimit_deep,
                  std::forward< ARGS >(args)... );
         }
   public :
      ...
   };

В коде deliver_op_tracer_t::make_trace самый важный момент -- это передача набора аргументов args хвостом списка аргументов при вызове details::make_trace(). В C++11 это делается буквально "по щелчку", никаких усилий со стороны программиста.

Теперь можно рассмотреть основную вспомогательную функцию details::make_trace. В ней вообще нет ничего сложного:

inline void
make_trace_to( std::ostream & ) {}

templatetypename A, typename... OTHER >
void
make_trace_to( std::ostream & s, A && a, OTHER &&... other )
   {
      make_trace_to_1( s, std::forward< A >(a) );
      make_trace_to( s, std::forward< OTHER >(other)... );
   }

templatetypename... ARGS >
void
make_trace(
   so_5::msg_tracing::tracer_t & tracer,
   ARGS &&... args ) SO_5_NOEXCEPT
   {
      so_5::details::invoke_noexcept_code( [&] {
            std::ostringstream s;

            s << "[tid=" << query_current_thread_id() << "]";

            make_trace_to( s, std::forward< ARGS >(args)... );

            tracer.trace( s.str() );
         } );
   }

Фактически, всю работу делают две функции make_trace_to и набор перегруженных функций make_trace_to_1, каждая из которых написана под свой тип аргумента, вроде вот такого:

inline void
make_trace_to_1( std::ostream & s, const std::type_index & msg_type )
   {
      s << "[msg_type=" << msg_type.name() << "]";
   }

inline void
make_trace_to_1( std::ostream & s, const agent_t * agent )
   {
      s << "[agent_ptr=" << agent << "]";
   }

inline void
make_trace_to_1( std::ostream & s, const state_t * state )
   {
      s << "[state=" << state->query_name() << "]";
   }

inline void
make_trace_to_1( std::ostream & s, const event_handler_data_t * handler )
   {
      s << "[evt_handler=";
      if( handler )
         s << handler;
      else
         s << "NONE";
      s << "]";
   }

Некоторые заморочки в реализации details::make_trace связаны с тем, что эта функция не должна выпускать наружу исключения. А убогие компиляторы, вроде Visual C++ 12.0, не поддерживают noexcept, поэтому приходится использовать парочку костылей.

Вот, в принципе, и все.

Хотя имеет смысл отметить еще один аспект. То, что есть набор функций make_trace_to_1, каждая из которых заточена под конкретный тип аргумента, -- это очень удобно (даже не смотря на далеко не самое удачное название). Некоторые из них за короткий период времени довольно сильно эволюционировали. Вот, например, сначала было так:

inline void
make_trace_to_1( std::ostream & s, const message_ref_t & message )
   {
      s << "[msg_ptr=" << message.get() << "]";
   }

Затем стало вот так (и это, возможно, еще не конечный вариант):

inline void
make_trace_to_1( std::ostream & s, const message_ref_t & message )
   {
      // The first pointer is a pointer to envelope.
      // The second pointer is a pointer to payload.
      using msg_pointers_t = std::tuple< const void *, const void * >;

      auto detect_pointers = [&message]() -> msg_pointers_t {
         ifconst message_t * envelope = message.get() )
            {
               // We can try cases with service requests and user-type messages.
               const void * payload =
                     internal_message_iface_t{ *envelope }.payload_ptr();

               if( payload != envelope )
                  // There are an envelope and payload inside it.
                  return msg_pointers_t{ envelope, payload };
               else
                  // There is only payload.
                  return msg_pointers_t{ nullptr, envelope };
            }
         else
            // It is a signal.
            return msg_pointers_t{ nullptrnullptr };
      };

      const void * envelope = nullptr;
      const void * payload = nullptr;

      std::tie(envelope,payload) = detect_pointers();

      if( envelope )
         s << "[envelope_ptr=" << envelope << "]";
      if( payload )
         s << "[payload_ptr=" << payload << "]";
      else
         s << "[signal]";
   }

При этом такое серьезное изменение отображения одного из параметров трассировочного сообщения не оказало совершенно никакого влияния на остальные части механизма трассировки.

PS. Кстати говоря, в коде make_trace_to_1 для message_ref_t можно увидеть использование класса internal_message_iface_t -- это живой пример подхода к разделению интерфейса класса на публичную и закрытую части посредством класса-френда.

Комментариев нет: