понедельник, 17 апреля 2017 г.

[prog.c++] Шаблоны против копипасты 6: просто шаблоны и просто перегрузка функций

Очередная часть серии "шаблоны против копипасты", которая, как обычно, получилась в результате рефакторинга имевшегося и работавшего кода (предыдущая часть серии здесь). Правда, в этом случае я буду показывать не реальный код, ибо тогда пост получится перегруженными никому не интересными деталями. А уже максимально упрощенная и сокращенная версия. Но все равно получилось довольно объемно, поэтому все фрагменты кода упрятаны под кат.

Итак, суть в том, что было несколько семейств функций send, send_delayed и send_periodic. В каждом семействе была одна "как бы главная" функция, получавшая самый общий набор аргументов. А все остальные функции были всего лишь обертками вокруг главной функции, но обертками, заточенными под определенный тип получателя. Обертки эти нужны были для того, чтобы сделать код, использующий send-ы, единообразным. Чтобы отсылка сообщения в какой-то конкретный mbox синтаксически не отличалась от отсылки сообщения в какой-то mchain. Ибо такое синтаксическое однообразие очень и очень сильно упрощает написание шаблонного кода.

В общем, за несколько лет развития данные семейства функций приобрели, схематично, следующий вид:

// Первое семейство функций.
//
template<typename... ARGS>
void send(mbox_t & dest, ARGS &&... args) {...}

template<typename... ARGS>
void send(agent_t & dest, ARGS &&... args) {
   send(dest.so_direct_mbox(), forward<ARGS>(args)...);
}

template<typename... ARGS>
void send(adhoc_agent_t & dest, ARGS &&... args) {
   send(dest.direct_mbox(), forward<ARGS>(args)...);
}

template<typename... ARGS>
void send(mchain_t & dest, ARGS &&... args) {
   send(dest.as_mbox(), forward<ARGS>(args)...);
}

// Второе семейство функций.
//
template<typename... ARGS>
void send_delayed(env_t & env, mbox_t & dest, duration pause, ARGS &&... args) {...}

template<typename... ARGS>
void send_delayed(agent_t & dest, duration pause, ARGS &&... args) {
   send_delayed(dest.so_environment(), dest.so_direct_mbox(), pause, forward<ARGS>(args)...);
}

template<typename... ARGS>
void send_delayed(adhoc_agent_t & dest, duration pause, ARGS &&... args) {
   send_delayed(dest.environment(), dest.direct_mbox(), pause, forward<ARGS>(args)...);
}

template<typename... ARGS>
void send_delayed(mchain_t & dest, duration pause, ARGS &&... args) {
   send_delayed(dest.environment(), dest.as_mbox(), pause, forward<ARGS>(args)...);
}

// Третье семейство функций.
//
template<typename... ARGS>
timer_t send_periodic(env_t & env, mbox_t & dest,
      duration pause, duration period, ARGS &&... args) {...}

template<typename... ARGS>
timer_t send_periodic(agent_t & dest,
      duration pause, duration period, ARGS &&... args) {
   return send_periodic(dest.so_environment(), dest.so_direct_mbox(),
         pause, period, forward<ARGS>(args)...);
}

template<typename... ARGS>
timer_t send_periodic(adhoc_agent_t & dest,
      duration pause, duration period, ARGS &&... args) {
   return send_periodic(dest.environment(), dest.direct_mbox(),
         pause, period, forward<ARGS>(args)...);
}

template<typename... ARGS>
timer_t send_periodic(mchain_t & dest,
      duration pause, duration period, ARGS &&... args) {
   return send_periodic(dest.environment(), dest.as_mbox(),
         pause, period, forward<ARGS>(args)...);
}

На самом деле все несколько сложнее и перегруженных функций чуть-чуть больше, но смысл именно такой.

Расширение каждого семейства происходило постепенно, поэтому изначально задача минимизации повторяющегося кода актуальной не была. Просто добавилась такая штука, как adhoc_agent_t, добавились три новых варианта send, send_delayed и send_periodic. Добавился mchain -- появились еще три варианта... Но вот когда все уже существующие варианты потребовалось чуть-чуть доработать до поддержки новой функциональности, то тут выяснилось, что обилие повторяющегося кода -- это совсем не хорошо.

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

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

// Вспомогательные функции-конвертеры.
//
namespace details {
   inline mbox_t & to_mbox(agent_t & dest) { return dest.so_direct_mbox(); }
   inline mbox_t & to_mbox(adhoc_agent_t & dest) { return dest.direct_mbox(); }
   inline mbox_t & to_mbox(mchain_t & dest) { return dest.as_mbox(); }

   inline env_t & to_env(agent_t & dest) { return dest.so_environment(); }
   inline env_t & to_env(adhoc_agent_t & dest) { return dest.environment(); }
   inline env_t & to_env(mchain_t & dest) { return dest.environment(); }
}

Ну а с их помощью создание шаблонных send, send_delayed и send_periodic уже оказывается делом техники. В итоге все три семейства функций начинают выглядеть вот так:

// Первое семейство функций.
//
template<typename... ARGS>
void send(mbox_t & dest, ARGS &&... args) {...}

template<typename D, typename... ARGS>
void send(D & dest, ARGS &&... args) {
   send(details::to_mbox(dest), forward<ARGS>(args)...);
}

// Второе семейство функций.
//
template<typename... ARGS>
void send_delayed(env_t & env, mbox_t & dest, duration pause, ARGS &&... args) {...}

template<typename D, typename... ARGS>
void send_delayed(D & dest, duration pause, ARGS &&... args) {
   send_delayed(details::to_env(dest), details::to_mbox(dest), pause, forward<ARGS>(args)...);
}

// Третье семейство функций.
//
template<typename... ARGS>
timer_t send_periodic(env_t & env, mbox_t & dest,
      duration pause, duration period, ARGS &&... args) {...}

template<typename D, typename... ARGS>
timer_t send_periodic(D & dest, duration pause, duration period, ARGS &&... args) {
   return send_periodic(details::to_env(dest), details::to_mbox(dest),
         pause, period, forward<ARGS>(args)...);
}

Что уже гораздо компактнее, а значит проще и дешевле в сопровождении. И, что очень немаловажно, в документировании :)

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

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