пятница, 21 ноября 2014 г.

[prog.c++] Про использование C++ных шаблонов при разработке timertt-1.1

Библиотека таймерных нитей, timertt, создавалась специально таким образом, чтобы моменты срабатывания таймеров отслеживались отдельной нитью. Т.е. клиент библиотеки регистрирует таймер и не заботится о том, кто и как будет отслеживать время срабатывания. И о том, на каком контексте будет вызван обработчик таймера когда наступит время обработки сработавшего таймера.

Однако, все это хорошо когда приложение многопоточное. Ну создается еще один дополнительный поток, ну и ничего страшного. Но захотелось адаптировать timertt к условиям, когда приложение выполняет все свои операции в одном рабочем потоке. Например, приложение работает на маломощном встроенном устройстве, где многопоточности нет в принципе, т.е. от слова совсем. Скажем, на каком-нибудь умном контроллере, который периодически опрашивает счетчики расхода воды.

Полагаю, что процесс преобразования timertt к поддержке как однопоточного, так и многопоточного варианта еще не закончен. Но какие-то работающие куски уже получены. Данный пост преследует цель попытаться лучше понять, что же именно было сделано, устраивает ли этот вариант и куда/как следует двигаться дальше. Ну и может быть кому-то будет интересно посмотреть на фокусы с шаблонами, на которые пришлось пойти, дабы минимизировать объем дублирующегося кода и избежать тупой копи-пасты.

Итак, что было в начале работы над версией 1.1?

Была крайне просто реализованная версия 1.0, в которой находилось три очень похожих друг на друга шаблонных класса: timer_wheel_thread_template_t, timer_list_thread_template_t, timer_heap_thread_template_t. Не скрою, при их реализации копи-паста использовалась, но в значительно меньшей степени, чем это может показаться на первый взгляд. Основная причина в том, что данная предметная область была для меня незнакомой (последний раз что-то подобное я делал в середине 90-х), а нужно было сделать быстро и надежно. Поэтому упор делался на работоспособность и понятность кода, с принесением в жертву красоты и, в какой-то степени, сопровождаемости.

Каждую реализацию таймерных механизмов условно можно разделить на три части. Во-первых, в каждой реализации есть уникальные куски кода, завязанные на специфику конкретного механизма. Например, в timer_wheel и timer_list используются свои собственные структуры данных, хотя и там, и там активно применяются двусвязные списки. А в timer_heap вообще совсем другой подход, там списков нет. Поэтому изрядную часть кода timer_threads занимали фрагменты, специфические для каждого таймерного механизма.

Во-вторых, в каждой реализации были куски кода, которые были полностью одинаковыми, и которые было легко выделить в общий, повторно используемый вид. Это то, что связано с запуском/остановом рабочих нитей (т.е. методы start(), shutdown(), join(), shutdown_and_join()). Для чего был создан класс thread_basic_t, в который все одинаковые методы и были помещены.

В-третьих, в каждой реализации были куски одинакового кода, который было бы желательно куда-то вынести, но на придумывание того, как это сделать, не было ни времени, ни большого желания. В первую очередь это касается практически всех методов activate(): в каждой таймерной нити есть всего один специфический activate(), реализация которого зависит от таймерного механизма, а все остальные варианты activate() просто вызывают его с теми или иными параметрами. Плюс еще была декларация перечисления timer_status_t, которая оказалась одинаковой и для timer_wheel, и для timer_list, но которую было лень выносить наружу.

Что нужно было получить в версии 1.1?

Нужно было получить новые классы timer_wheel_manager_template, timer_list_manager_template и timer_heap_manager_template, которые внутри себя использовали бы те же самые механизмы, что и timer_threads. Но классы timer_managers хотелось иметь в двух вариантах. Первый вариант не должен обеспечивать thread safety вообще. Т.е. все операции над timer_manager можно выполнять только с контекста одной нити. Например, добавлять и удалять таймеры можно только на той нити, где работает timer_manager. Этот вариант как раз интересен для случая встроенных устройств, где многопоточность не поддерживается.

Второй вариант должен обеспечивать thread safety для таких операций, как добавление и удаление таймеров. Этот вариант предназначен для случаев, когда timertt используется в многопоточном приложении, но пользователь хочет локализовать обработку таймерных заявок на свой собственной рабочей нити. А вот создавать/удалять таймеры могут и другие нити приложения.

Т.е. для каждого таймерного механизма в timertt должно было бы появиться, условно говоря, три класса: timer_thread, thread_unsafe_manager и thread_safe_manager. Очевидно, что если не уделять внимания повторному использованию кода в этих классах, то объем копи-пасты вырастет до совершенно неприличных масштабов и сопровождать такой код будет либо крайне затруднительно, либо вообще невозможно.

Вот именно ради избежания дублирования одинаковых кусков кода и были активно задействованы C++ные шаблоны. Последующий текст расскажет что и почему было сделано. Текущий вариант исходного кода можно найти в Svn-репозитории. Вероятно, этот вариант не окончательный и код еще будет переделываться. Однако, описанные ниже приемы оказались для меня интересны сами по себе.

Первый, довольно простой вопрос -- это как быть с самими объектами-таймерами? Ведь в них есть следы многопоточности. Например, атомарные счетчики ссылок и атомарные статусы.

Этот вопрос был решен за счет превращения класса timer_t, являющегося базой для всех таймерных объектов, в шаблонный класс timer_object:

templatetypename THREAD_SAFETY >
struct timer_object { ... };

Параметром шаблона является признак того, идет ли работа в исключительно однопоточной или же в многопоточной среде. Что задается посредством типов thread_safety::unsafe и thread_safety::safe:

struct thread_safety
{
   struct unsafe {};
   struct safe {};
};

Первоначально вместо типов unsafe/safe использовались элементы перечисления. Но затем все-таки были выбраны типы, дабы не объявлять static const объект в одном из вспомогательных классов, а обойтись не имеющим никакой стоимости в runtime привычным typedef-ом. Этот момент еще будет показан ниже.

Для того, чтобы timer_object корректно определялся как в однопоточной, так и в многопоточной среде, был сделал вот такой шаблонный класс:

templatetypename THREAD_SAFETY >
struct threading_traits {};

template<>
struct threading_traits< thread_safety::unsafe >
{
   typedef unsigned int reference_counter_type;

   typedef details::timer_status status_holder_type;
};

template<>
struct threading_traits< thread_safety::safe >
{
   typedef std::atomic_uint reference_counter_type;

   typedef std::atomic< details::timer_status > status_holder_type;
};

Который в timer_object используется вот так:

templatetypename THREAD_SAFETY >
struct timer_object
{
   typename threading_traits< THREAD_SAFETY >::reference_counter_type m_references;

   inline timer_object() { m_references = 0; }

   inline virtual ~timer_object() {}

   static inline void
   increment_references( timer_object * t )
   {
      ++(t->m_references);
   }

   static inline void
   decrement_references( timer_object * t )
   {
      if0 == --(t->m_references) )
         delete t;
   }
};

Т.е. для случая timer_object<thread_safety::unsafe> счетчик ссылок будет обычным unsigned int-ом. Тогда как для случая thread_safety::safe это уже будет атомарный счетчик, над которым можно будет безопасно выполнять операции из разных нитей.

Так же в threading_traits определяется тип хранилища для статуса таймерной заявки. Для однопоточного режима -- это простой объект timer_status, а для многопоточного -- std::atomic<timer_status>.

Вторым вопросом, который нужно было решить, был вопрос о вынесении куда-то самих таймерных механизмов, чтобы с ними могли работать как timer_threads, так и timer_managers.

Это было сделано посредством шаблонных классов timer_engines: timer_wheel_engine, timer_list_engine и timer_heap_engine. Эти классы не связаны друг с другом отношениями наследования, но имеют одинаковые публичные методы, т.е. они структурно эквивалентны друг другу. Обычный для C++ прием. Например, если посмотреть на классы-контейнеры из STL, то можно увидеть, что у них есть идентичные по формату методы (те же begin(), end(), empty() и т.д.), но при этом ни std::vector, ни std::list не наследуются от какого-то общего интерфейса. Аналогично сделано и в timertt, где каждый timer_engine имеет одинаковый набор публичных методов:

timer_object_holder< THREAD_SAFETY > allocate();

templateclass DURATION_1, class DURATION_2 >
bool activate(
   timer_object_holder< THREAD_SAFETY > timer,
   DURATION_1 pause,
   DURATION_2 period,
   timer_action action );

void deactivate( timer_object_holder< THREAD_SAFETY > timer );

templatetypename UNIQUE_LOCK >
void process_expired_timers( UNIQUE_LOCK & lock );

bool empty() const;

monotonic_clock::time_point nearest_time_point() const;

void clear_all();

Ключевых моментов здесь два. Первый, самый простой, это привязка timer_engine к параметру THREAD_SAFETY. Это необходимо, т.к. timer_engine создает конкретные timer_object-ы. И они должны создаваться правильным образом: для однопоточной среды без средств защиты от многопоточности, а в многопоточной среде -- с оными.

Второй момент более сложный. Он связан с защитой внутренностей timer_engine от многопоточности. Должен ли timer_engine иметь свой собственный mutex или нет?

Было принято решение о том, что внутри timer_engine нет никаких примитивов синхронизации. Если timer_engine используется внутри timer_thread или внутри многопоточного timer_manager-а, то владелец timer_engine должен обеспечить должную защиту. Тут возникает некоторая проблема.

Когда timer_thread/timer_manager дергает метод process_expired_timers у engine, часть действий нужно выполнить на заблокированном объекте, а часть -- на разблокированном. Сначала timer_engine ищет сработавшие таймеры, тут timer_engine должен быть заблокирован. Затем timer_engine запускает обработчики, тут timer_engine должен быть разблокирован, т.к. возможно обращение к timer_engine из запущенного обработчика. Затем timer_engine опять должен быть заблокирован, чтобы переставить время срабатывания для периодических таймеров и утилизировать отработавшие одноразовые таймеры.

Получается, что timer_engine должен иметь доступ к внешнему замку, точного типа которого он не знает. Да и в случае использования timer_engine из однопоточного timer_manager, этого замка вообще не будет как такового. Будет просто какая-то пустая заглушка.

Для решения этой проблемы метод process_expired_timers в timer_engine был сделан шаблонным. Т.е. получилось, что в шаблонном классе timer_engine есть еще и шаблонный метод:

template<
   typename THREAD_SAFETY,
   typename ERROR_LOGGER,
   typename ACTOR_EXCEPTION_HANDLER >
class timer_wheel_engine
   :  public engine_common<
         THREAD_SAFETY, ERROR_LOGGER, ACTOR_EXCEPTION_HANDLER >
{
public :
   templatetypename UNIQUE_LOCK >
   void process_expired_timers( UNIQUE_LOCK & lock )

В принципе, можно было бы сделать вместо одного process_expired_timers группу методов: find_expired_timers, process_expired_timers, utilize_expired_timers. И владелец timer_engine сам бы работал со своим примитивом синхронизации: сначала вызывал find_expired_timers, затем разблокировался бы и вызывал process_expired_timers, затем блокировался и вызывал utilize_expired_timers. Но такой подход не очень удобен для timer_heap_engine, где пришлось бы заводить отдельную структуру данных для хранения и последующей обработки сработавших таймеров. Поэтому было решено обойтись одним показанным выше process_expired_timers.

Третьим вопросом был вопрос об устранении дублирования кода между timer_managers и timer_threads.

Очевидно, что у timer_managers и timer_threads будут как пересекающиеся методы (allocate, activate, deactivate), так и непересекающиеся (reset/process_expired_timers у timer_managers, start/shutdown/join/shutdown_and_join у timer_threads). Нужно было каким-то образом сделать так, чтобы код пересекающихся методов был написан один раз, а затем переиспользован. Для этого методы allocate/activate/deactivate были вынесены в общий вспомогательный класс basic_methods_impl_mixin, от которого уже наследуются классы timer_managers и timer_threads.

Однако, классу basic_methods_impl_mixin нужно знать несколько вещей:

Во-первых, какой timer_engine будет использоваться. Это решается за счет параметра шаблона для basic_methods_impl_mixin. Пожалуй, это единственный нормальный вариант. Если бы timer_engines реализовывали какой-то общий абстрактный интерфейс, тогда можно было бы basic_methods_impl_mixin завязывать на интерфейс и передавать в конструктор, скажем, динамически созданный экземпляр конкретного timer_engine. Но, т.к. такого абстрактного интерфейса нет, то связывание basic_methods_impl_mixin и timer_engine происходит посредством параметра шаблона:

template<
   typename ENGINE,
   typename CONSUMER >
class basic_methods_impl_mixin

Во-вторых, какие примитивы синхронизации должны использоваться для защиты от многопоточности. И нужны ли они вообще.

Если basic_methods_impl_mixin будет частью однопоточного timer_manager-а, то никакие примитивы синхронизации не нужны. Если частью многопоточного timer_manager-а, то нужен mutex. Если же частью timer_thread, то кроме mutex-а нужно иметь еще и condition_variable для того, чтобы вовремя будить рабочую нить таймера. Впрочем, все эти детали лучше прояснить на примере кода основного метода activate:

templateclass DURATION_1, class DURATION_2 >
void
activate(
   timer_holder timer,
   DURATION_1 pause,
   DURATION_2 period,
   timer_action action )
{
   typename mixin_type::lock_guard locker{ *this };

   this->ensure_started();

   if( m_engine.activate(
         std::move( timer ), pause, period, std::move( action ) ) )
      this->notify();
}

Для начала рассмотрим, как этот метод работает в случае timer_thread:

Сначала объект блокируется. Для этого используется идиома RAII и временный объект locker. Затем проверяется, реально ли нить запущена или нет. Если не запущена, то пользователю выбрасывается исключение. Этим занимается метод ensure_started. Затем выполняется регистрация таймера внутри timer_engine. Если выясняется, что у нового таймера самое ранее время срабатывания, то рабочая нить информируется об этом, чтобы она могла скорректировать время своего пробуждения. За это отвечает метод notify.

Что будет, если этот метод будет работать в рамках многопоточного timer_manager:

Сначала объект блокируется. Тут все тоже самое, объект locker выполняет свою роль. А вот проверять запущенность нити не нужно. Следовательно метод ensure_started() не нужен. Обращение к m_engine.activate() остается. Но будить рабочую нить не нужно, поэтому и метод notify() не нужен.

В случае с однопоточным timer_manager-ом все еще проще:

Объект блокировать не нужно, значит locker лишний. Как и обращение к ensure_started(). Как и обращение к notify(). Нужно оставить только вызов m_engine.activate().

Соответственно, вопрос в том, как выбрасывать лишнее из кода метода activate для разных ситуаций. Либо на уровне самого исходного кода. Т.е., грубо говоря так:

#if defined( TIMER_THREAD ) || defined( TIMER_MANAGER )
   typename mixin_type::lock_guard locker{ *this };
#endif

#if defined( TIMER_THREAD )
   this->ensure_started();
#endif

   if( m_engine.activate(
         std::move( timer ), pause, period, std::move( action ) ) )
#if defined( TIMER_THREAD )
      this->notify();
#else
      ;
#endif

Либо каким-то другим образом, оставляя код без изменений.

Понятное дело, что код остался один и тот же для всех сценариев. Но вот реализация locker-а, ensure_started() и notify() меняется. Для некоторых сценариев это пустые заглушки, обращения к которым выбрасываются нормальным оптимизирующим компилятором. Вопрос лишь в том, каким образом снабдить basic_methods_impl_mixin нужными реализациями этих методов. Для этого создано несколько разных базовых типов для разных сценариев:

Для случая однопоточного timer_manager-а все является пустыми заглушками:

struct thread_unsafe_manager_mixin
{
   class lock_guard
   {
   public :
      lock_guard( thread_unsafe_manager_mixin & ) {}

      void lock() {}
      void unlock() {}
   };

   void
   ensure_started() {}

   void
   notify() {}
};

Для многопоточного timer_manager-а нужен реальный mutex и выполняющий свою работу lock_guard:

struct thread_safe_manager_mixin
{
   std::mutex m_lock;

   class lock_guard
   {
      std::unique_lock< std::mutex > m_lock;

   public :
      lock_guard( thread_safe_manager_mixin & self )
         : m_lock( self.m_lock )
      {}

      void lock() { m_lock.lock(); }
      void unlock() { m_lock.unlock(); }
   };

   void
   ensure_started() {}

   void
   notify() {}
};

Для timer_thread нужно еще больше реальных внутренностей:

struct thread_mixin
{
   std::mutex m_lock;

   std::condition_variable m_condition;

   std::shared_ptr< std::thread > m_thread;

   class lock_guard
   {
      std::unique_lock< std::mutex > m_lock;

   public :
      lock_guard( thread_mixin & self )
         : m_lock( self.m_lock )
      {}

      void lock() { m_lock.lock(); }
      void unlock() { m_lock.unlock(); }

      std::unique_lock< std::mutex > &
      actual_lock() { return m_lock; }
   };

   void
   ensure_started()
   {
      if( !m_thread )
         throw std::runtime_error( "timer thread is not started" );
   }

   void
   notify()
   {
      m_condition.notify_one();
   }
};

Ну а выбор соответствующего базового класса для basic_methods_impl_mixin -- это уже дело техники, основанной на специализации C++ шаблонов:

templatetypename THREAD_SAFETY, typename CONSUMER >
struct mixin_selector { };

template<>
struct mixin_selector< thread_safety::unsafe, consumer_type::manager >
{
   using type = thread_unsafe_manager_mixin;
};

template<>
struct mixin_selector< thread_safety::safe, consumer_type::manager >
{
   using type = thread_safe_manager_mixin;
};

template<>
struct mixin_selector< thread_safety::safe, consumer_type::thread >
{
   using type = thread_mixin;
};

Этот класс mixin_selector используется при указании списка базовых классов basic_methods_impl_mixin:

template<
   typename ENGINE,
   typename CONSUMER >
class basic_methods_impl_mixin
   :  protected mixin_selector< typename ENGINE::thread_safety, CONSUMER >::type
   ,  public ENGINE::defaults_type

В качестве параметра CONSUMER указывается тип consumer_type::thread или consumer_type::manager.

Про еще один базовый тип для basic_methods_impl_mixin -- ENGINE::defaults_type -- речь пойдет чуть позже. Пока же нужно пояснить, каким образом определяется THREAD_SAFETY для basic_methods_impl_mixin: каждый timer_engine делает у себя typedef с именем thread_safety. Что и позволяет передавать в basic_methods_impl_mixin всего два параметра шаблона -- тип timer_engine и тип владельца timer_engine. А уже THREAD_SAFETY автоматически извлекается из timer_engine. Кстати, дабы не вписывать вручную typedef для thread_safety в каждом timer_engine используется специальный вспомогательный базовый класс:

template<
   typename THREAD_SAFETY,
   typename ERROR_LOGGER,
   typename ACTOR_EXCEPTION_HANDLER >
class engine_common
{
public :
   //! Indicator of thread-safety.
   using thread_safety = THREAD_SAFETY;

   //! Initializing constructor.
   engine_common(
      ERROR_LOGGER error_logger,
      ACTOR_EXCEPTION_HANDLER exception_handler )
      :  m_error_logger( error_logger )
      ,  m_exception_handler( exception_handler )
   {}

protected :
   //! Error logger.
   ERROR_LOGGER m_error_logger;

   //! Exception handler.
   ACTOR_EXCEPTION_HANDLER m_exception_handler;
};

Кстати говоря, для того, чтобы в engine_common имя thread_safety было именем типа, а не static const объектом, маркеры thread_safety::unsafe и thread_safety::safe сделаны типами, а не элементами перечисления.

Четвертый момент -- это реализация timer_managers и timer_threads. Эта реализация в текущем варианте разбита на два уровня. На первом уровне находятся шаблоны manager_impl_template и thread_impl_template:

templatetypename ENGINE >
class manager_impl_template
   :  public basic_methods_impl_mixin< ENGINE, consumer_type::manager > 
{
   //! Shorthand for base type.
   using base_type = basic_methods_impl_mixin<
         ENGINE,
         consumer_type::manager >;

public :
   //! Constructor with all parameters.
   templatetypename... ARGS >
   manager_impl_template(
      ARGS && ... args )
      :  base_type( std::forward< ARGS >(args)... )
   {
   }
   ...
};

templatetypename ENGINE >
class thread_impl_template
   :  public basic_methods_impl_mixin< ENGINE, consumer_type::thread > 
{
   //! Shorthand for base type.
   using base_type = basic_methods_impl_mixin<
         ENGINE,
         consumer_type::thread >;

public :
   //! Constructor with all parameters.
   templatetypename... ARGS >
   thread_impl_template(
      ARGS && ... args )
      :  base_type( std::forward< ARGS >(args)... )
   {
   }
   ...
};

А уже на втором уровне находятся timer_managers и timer_threads, привязанные к конкретным таймерным механизмам. Например, к timer_wheel:

template<
   typename ERROR_LOGGER,
   typename ACTOR_EXCEPTION_HANDLER >
class timer_wheel_thread_template
   : public
      details::thread_impl_template<
            details::timer_wheel_engine<
                  thread_safety::safe,
                  ERROR_LOGGER,
                  ACTOR_EXCEPTION_HANDLER > > 
...

template<
   typename THREAD_SAFETY,
   typename ERROR_LOGGER = default_error_logger,
   typename ACTOR_EXCEPTION_HANDLER = default_actor_exception_handler >
class timer_wheel_manager_template
   : public
      details::manager_impl_template<
            details::timer_wheel_engine<
                  THREAD_SAFETY,
                  ERROR_LOGGER,
                  ACTOR_EXCEPTION_HANDLER > > 

Причина такой двухуровневости в том, что в Visual C++ 2013 нет поддержки наследования конструкторов. Если бы таковая была, возможно, все ограничилось бы просто классами manager_impl_template и thread_impl_template, а сущности вроде timer_wheel_manager_template были бы typedef-ами.

Но в отсутствии наследования конструкторов приходится решать проблему конструкторов с разными списками параметров. Например, для timer_wheel_manager нужно три конструктора: 1) без параметров, 2) с параметрами wheel_size и granularity, 3) с полным списком параметров -- wheel_size, granularity, error_logger и exception_handler. Причем эти конструкторы специфические именно для механизма timer_wheel, у механизма timer_list этот список уже другой.

Эта проблема пока решена в лоб: в классах manager_impl_template и thread_impl_template, а так же в классе basic_methods_impl_mixin, есть всего один конструктор, на основе variadic templates:

templatetypename... ARGS >
manager_impl_template(
   ARGS && ... args )
   :  base_type( std::forward< ARGS >(args)... )
{
}

templatetypename... ARGS >
thread_impl_template(
   ARGS && ... args )
   :  base_type( std::forward< ARGS >(args)... )
{
}

templatetypename... ARGS >
basic_methods_impl_mixin(
   ARGS && ... args )
   :  m_engine( std::forward< ARGS >(args)... )
{
}

А вот в конкретных классах (т.к. timer_wheel_manager_template, timer_wheel_thread_template) пишутся те конструкторы, которые необходимы конкретном таймерному механизму. Например, timer_wheel_manager_template делает это так:

//! Default constructor.
timer_wheel_manager_template()
   :  timer_wheel_manager_template(
         base_type::default_wheel_size(),
         base_type::default_granularity(),
         ERROR_LOGGER(),
         ACTOR_EXCEPTION_HANDLER() )
{}

//! Constructor with wheel size and granularity parameters.
timer_wheel_manager_template(
   //! Size of the wheel.
   unsigned int wheel_size,
   //! Size of time step for the timer_wheel.
   monotonic_clock::duration granularity )
   :  timer_wheel_manager_template(
         wheel_size,
         granularity,
         ERROR_LOGGER(),
         ACTOR_EXCEPTION_HANDLER() )
{}

//! Constructor with all parameters.
timer_wheel_manager_template(
   //! Size of the wheel.
   unsigned int wheel_size,
   //! Size of time step for the timer_wheel.
   monotonic_clock::duration granularity,
   //! An error logger for timer thread.
   ERROR_LOGGER error_logger,
   //! An actor exception handler for timer thread.
   ACTOR_EXCEPTION_HANDLER exception_handler )
   :  base_type(
         wheel_size,
         granularity,
         error_logger,
         exception_handler )
{}

В timer_wheel_thread_template конструкторы точно такие же, только имена отличаются. Так что в этом месте копи-паста присутствует и избавиться от нее, сохраняя поддержку Visual C++ 2013, не получилось.

Ну и пятый момент, на котором бы хотелось заострить внимание читателей, -- это повторное использование параметров по-умолчанию для разных таймерных механизмов.

Таким механизмам, как timer_wheel и timer_heap нужны значения для инициализации внутренних структур данных. У timer_wheel это wheel_size. У timer_heap это initial_heap_size. Кроме того, для работы timer_wheel нужен еще и параметр granularity -- размер одного рабочего шага. Откуда timer_manager/timer_thread возьмет эти параметры в случае, если пользователь задействует конструктор по-умолчанию? Получается, что timer_managers и timer_threads должны уметь каким-то образом получать дефолтные значения для конкретных таймерных механизмов.

Сделано это через простые вспомогательные структуры:

struct timer_wheel_engine_defaults
{
   inline static unsigned int
   default_wheel_size() { return 1000; }

   inline static monotonic_clock::duration
   default_granularity() { return std::chrono::milliseconds( 10 ); }
};

struct timer_list_engine_defaults
{
};

struct timer_heap_engine_defaults
{
   inline static std::size_t
   default_initial_heap_capacity() { return 64; }
};

Каждый timer_engine делает специальный typedef с именем defaults_type для соответствующей структуры данных, например:

template<
   typename THREAD_SAFETY,
   typename ERROR_LOGGER,
   typename ACTOR_EXCEPTION_HANDLER >
class timer_heap_engine
   :  public engine_common<
         THREAD_SAFETY, ERROR_LOGGER, ACTOR_EXCEPTION_HANDLER >
{
public :
   //! Type with default parameters for this engine.
   typedef timer_heap_engine_defaults defaults_type;

Ну а далее этот typedef с именем defaults_type используется для того, чтобы ввести содержимое вспомогательной структуры вовнутрь basic_methods_impl_mixin (а оттуда и в timer_managers, и в timer_threads):

template<
   typename ENGINE,
   typename CONSUMER >
class basic_methods_impl_mixin
   :  protected mixin_selector< typename ENGINE::thread_safety, CONSUMER >::type
   ,  public ENGINE::defaults_type

Т.е. если basic_methods_impl_mixin создается для timer_heap_engine, то этот basic_methods_impl_mixin будет унаследован, в том числе, и от timer_heap_engine_defaults. А это означает, что метод timer_heap_engine_defaults::default_initial_heap_size() станет частью соответствующих timer_heap_manager/timer_heap_thread.


Вот, пожалуй, и все, что хотелось рассказать. Как мне представляется, возможности C++11 здесь были задействованы довольно сильно. А будь в Visual C++ поддержка C++11 получше, то и еще сильнее. Думается, что без таких возможностей, реализация подобных классов оказалась бы сложнее/объемнее. И, не исключено, пришлось бы задействовать какую-то магию на макросах. Но, за счет мощности C++11, препроцессор используется только для директив #include :)


PS. Если вы смогли дочитать до этого места и текст показался интересным/полезным, то буду признателен за распространение ссылки на этот пост (например, нажав на кнопочку "+1"). Ну и, конечного же, вопросы/замечания/соображения всячески приветствуются.

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