Вчера написал коротенькую заметку про то, что пора уже пристально смотреть в сторону 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:
template< typename 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. Чем и воспользовался:
template< typename 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 и т.д.):
template< typename 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...
Комментариев нет:
Отправить комментарий