суббота, 30 января 2016 г.

[prog.c++] Стоило вчера помянуть C++14 как сегодня его возможности оказались бы очень в тему :)

Вчера написал коротенькую заметку про то, что пора уже пристально смотреть в сторону C++14, как сегодня столкнулся с ситуацией, где возможности C++14 сократили бы и упростили бы мой собственный код.

Понадобилось создать вспомогательную шаблонную функцию make_disp_params, которая создавала бы объект некоторого типа disp_params_t, вызывала бы у него метод tune_queue_params, после чего возвращала бы этот объект disp_params_t.

Загвоздка была в том, что есть несколько типов disp_params_t, определенных в разных пространствах имен. Это разные типы, хотя и имеющие много похожих методов (точно такой же трюк применен в C++ном STL-е, где, к примеру, методы push_back есть и у std::vector, и у std::list, и у std::deque, хотя эти типы между собой не связаны).

Так вот, в каждом из эти пространств имен рядом с disp_params_t есть вложенное пространство имен queue_traits, а в нем структура queue_params_t. Типы queue_params_t разные. Но с похожими методами :)

Т.е. есть что-то вроде:

namespace A {
   namespace queue_traits {
      struct queue_params_t {
         void lock_factory(...);
         ...
      };
   }
   struct disp_params_t {
      void tune_queue_params(...);
      ...
   };
}
namespace B {
   namespace queue_traits {
      struct queue_params_t {
         void lock_factory(...);
         ...
      };
   }
   struct disp_params_t {
      void tune_queue_params(...);
      ...
   };
}

Но это пока еще не загвоздка, а просто общий фон. Загвоздка же в том, что методы tune_queue_params получают лямбда-функцию. Единственным аргументом для которой является ссылка на соответствующий queue_params_t.

Т.е. внутри make_disp_params мне нужно было вызывать у disp_params_t метод tune_queue_params и передать в него некую лямбду с аргументом некоторого типа. Но какого именно типа? Ведь известен только тип disp_params_t, но вовсе не тип queue_params_t из соответствующего пространства имен...

В C++14 с этим вообще нет проблем, от слова совсем. Пишем простую полиморфную лямбду. Как я, собственно и сделал в первом варианте make_disp_params:

templatetypename DISP_PARAMS, typename COMBINED, typename SIMPLE >
auto make_disp_params(
   const cfg_t & cfg,
   COMBINED combined_factory, SIMPLE simple_factory )
   {
      DISP_PARAMS disp_params;
      disp_params.tune_queue_params( [&]( auto & p ) {...} );
      return disp_params;
   }

Лепота! Но нужно было остаться в рамках C++11, в котором полиморфных лямбд нет. Что делать?

Простым выходом было бы добавить в make_disp_params еще один шаблонный параметр. Но это некрасиво, т.к. приходится выносить наверх зависимость, которую не следовало бы показывать. К счастью, внутри disp_params_t был метод, из сигнатуры которого можно было бы вывести тип queue_params_t. Чем и воспользовался:

templatetypename DISP_PARAMS, typename COMBINED, typename SIMPLE >
DISP_PARAMS make_disp_params(
   const cfg_t & cfg,
   COMBINED combined_factory, SIMPLE simple_factory )
   {
      DISP_PARAMS disp_params;
      using QUEUE = std::decay< decltype(disp_params.queue_params()) >::type;

      disp_params.tune_queue_params( [&]( QUEUE & p ) {...} );
      return disp_params;
   }

А вот если бы такого метода не было, то пришлось бы все-таки передавать в make_disp_params дополнительный параметр. Но метод все-таки был, посему использование make_disp_params выглядит вот так:

using namespace so_5::disp::one_thread;
auto disp_params = make_disp_params< disp_params_t >(
      cfg,
      []{ return queue_traits::combined_lock_factory(); },
      []{ return queue_traits::simple_lock_factory(); } );
return create_private_disp( env, "disp", disp_params )->binder();

Вот так и ловишь себя на том, что C++14 может принести заметную помощь здесь и сейчас :)


Ну и дабы не плодить лишних блог постов, затрону еще несколько тем, которые имеют непосредственное отношение к использованию современного C++.


Иногда сожалеешь, что не хватает возможности передать параметром в шаблон не тип, а пространство имен.

Когда это может быть полезно? Да как раз в примере, расмотренном выше.

Представим, что у меня было бы вот так:

struct A {
   struct queue_traits {
      struct queue_params_t {
         void lock_factory(...);
         ...
      };
   }
   struct disp_params_t {
      void tune_queue_params(...);
      ...
   };
}
struct B {
   struct queue_traits {
      struct queue_params_t {
         void lock_factory(...);
         ...
      };
   }
   struct disp_params_t {
      void tune_queue_params(...);
      ...
   };
}

Я бы мог тогда написать шаблоную функцию make_disp_params используя соглашение об именовании (по аналогии с тем, как в STL определяются имена вида vector::reference, list::reference, deque::reference и т.д.):

templatetypename NS, typename COMBINED, typename SIMPLE >
NS::disp_params_t make_disp_params(
   const cfg_t & cfg,
   COMBINED combined_factory, SIMPLE simple_factory )
   {
      NS::disp_params_t disp_params;
      disp_params.tune_queue_params( [&]( NS::queue_traits::queue_params_t & p ) {...} );
      return disp_params;
   }

Трюк со вложенными структурами очень старый. Я им пользовался в 90-х, когда приходилось иметь дело с компиляторами, в которых не было поддержки пространств имен. Но данный трюк не так удобен для повседневного использования, т.к. для пространств имен можно сделать using namespace, а для вложенных структур -- нет...

Посему и есть сейчас такая вот дилемма: размещать какие-то описания внутри пространства имен или внутри какого-то типа. А вот если бы пространство имен можно было бы отдать параметром шаблона... :)


Ну и, конечно же, сложно отказать себе в удовольствии еще раз подкинуть дровишек в спор "ламповая сишечка против богомерзких крестов" ;)

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

so_5::disp_binder_unique_ptr_t
create_disp_binder(
   so_5::environment_t & env,
   const cfg_t & cfg )
   {
      const auto t = cfg.m_dispatcher_type;
      if( dispatcher_type_t::one_thread == t )
      {
         using namespace so_5::disp::one_thread;
         auto disp_params = make_disp_params< disp_params_t >(
               cfg,
               []{ return queue_traits::combined_lock_factory(); },
               []{ return queue_traits::simple_lock_factory(); } );
         return create_private_disp( env, "disp", disp_params )->binder();
      }
      else if( dispatcher_type_t::thread_pool == t )
      {
         using namespace so_5::disp::thread_pool;
         auto disp_params = make_disp_params< disp_params_t >(
               cfg,
               []{ return queue_traits::combined_lock_factory(); },
               []{ return queue_traits::simple_lock_factory(); } );
         return create_private_disp( env, "disp", disp_params )->binder(
               []( bind_params_t & p ) {
                  p.fifo( fifo_t::individual );
               } );
      }
      else
      {
         using namespace so_5::disp::prio_one_thread::strictly_ordered;
         auto disp_params = make_disp_params< disp_params_t >(
               cfg,
               []{ return queue_traits::combined_lock_factory(); },
               []{ return queue_traits::simple_lock_factory(); } );
         return create_private_disp( env, "disp", disp_params )->binder();
      }
   }

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

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

Так что, с моей точки зрения, если сложность прикладной проблемы сочетается с требованиями к производительности, то выбор C вместо C++ -- это выбор настоящего мазохиста, знающего толк в извращениях :)

Имхо, в сложных задачах с высокими требованиями к скорости и ресурсопотреблению С не является конкурентом C++. Как и Go. Как и, отчасти, Rust (из-за своеобразного ООП, из-за отсутствия исключений). Вообще в нише нативных языков с C++ в этой части могут потягаться разве что D (что не удивительно) и что-нибудь из функциональщины, вроде OCaml или Haskell-я. Хотя все это языки с GC и как бы там не пришлось отдельно бороться с GC...

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