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

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

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

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

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

четверг, 20 ноября 2014 г.

[prog.c++] Давно так плотно не работал с шаблонами, интересные впечатления

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

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

и вот такого:

template< typename THREAD_SAFETY, typename ERROR_LOGGER, typename ACTOR_EXCEPTION_HANDLER >
class timer_heap_manager_template
   : public details::manager_impl_template<
            details::timer_heap_engine<
                  THREAD_SAFETY,
                  ERROR_LOGGER,
                  ACTOR_EXCEPTION_HANDLER > > 
{

встречается регулярно :)

Проверяя написанное неоднократно ловил себя на мысли, что программирование на C++ных шаблонах -- это как программирование на динамически-типизированном языке вроде Ruby. Т.е. если не напишешь unit-тесты на все куски шаблонного класса, ничего о корректности своего кода сказать не можешь. Причем, если в коде нет реального инстанциирования шаблона, то некоторые компиляторы (не будем лишний раз тыкать палочкой в VC++, но...) пропускают чуть ли не синтаксические ошибки в коде :)

Еще одно впечатление: похоже, при написании кода шаблона самый лучший способ обращения к атрибутам и методам шаблонного класса -- это через this->. Т.е. во многих случаях можно писать так:

void
shutdown()
{
   typename base_type::lock_guard locker{ *this };

   if( m_thread && !m_shutdown )
   {
      m_shutdown = true;
      notify();
   }
}

Но, если часть атрибутов/методов со временем переезжает в базовый шаблонный класс, то для обращения к ним уже нужно использовать this:

void
shutdown()
{
   typename base_type::lock_guard locker{ *this };

   ifthis->m_thread && !this->m_shutdown )
   {
      this->m_shutdown = true;
      this->notify();
   }
}

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

Еще несколько расстраивает многословность шаблонов при их специализации. Т.е. сейчас приходится писать:

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;
};

Тогда как хотелось бы обойтись без лишних пустых деклараций. Что-то вроде:

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

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

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

Только вот не уверен, насколько это возможно. И видит ли в этом недостаток еще кто-нибудь кроме меня :)

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

Лично мне в этой связи часто вспоминается появление Java. Сейчас мало кто помнит, но шаблоны появились в C++ в конце 1980-х, самом начале 1990-х (при том, что официальной датой рождения C++ считается 1986-й год). Конечно, массовой поддержки шаблонов в C++ных компиляторах не было, да и там, где она была, были сильные различия в качестве ее реализации. Однако факты именно таковы: Степанов в 1993-м уже демонстрировал миру свои наработки по STL на базе C++ных шаблонов. Тогда как сама Java в Sun-е в это время существовала в виде наработок в рамках Green- и Oak-проектов.

А решение о выпуске ее в свет, да еще под именем Java, было принято в 1994-м. После чего началась рекламно-пропагандистская компания по подготовке мира к принятию Java. Сколько тогда было многообещающих статей о том, что Sun выкинул из C++ все самое плохое, оставил все самое лучшее, да еще и добавил сборщик мусора и реальную бинарную кроссплатформенность. А что получилось по факту? Как по мне, как сильно кастрированное и унылое говно. Гослинг со товарищи правильно сделали, что выбросили препроцессор и совместимость с C. Но вот то, что они не захотели сделать в Java шаблоны, да еще тогда, когда уже было видно, что шаблоны -- это одна из самых мощных возможностей C++... Иначе как ниасиляторством обозвать это нельзя.

Собственно, я вполне хорошо понимаю людей, которые набили себе шишек на "C with classes" и первых версиях Java (а то и C#) и ушли в OCaml-ы с Haskell-ем. Но, сдается мне, далеко не всегда у них были хорошие знания нормального C++, который с шаблонами и исключениями. Впрочем, это уже совсем другая тема для разговора.

[prog.flame] Очередной молодой программист вопрошает "Ну когда же все будет делаться нормально?"

Небезызвестный в Рунете персонаж выдал очередной поток сознания: "Объясните дураку". Что забавно: для того, чтобы зацепить читателя и проиллюстрировать свою мысль, он приводит в пример плейеры, которые нормально не могут сохранить точку остановки воспроизведения. Но зачем так далеко ходить за примерами? У него в ЖЖ настолько угребищный дизайн, что в больших обсуждениях комментарии очень быстро становятся нечитаемыми :)

Что же до основного вопроса, т.е. когда же софт будет разрабатываться нормально и когда появятся инструменты, которые упростят этот процесс, то ответ прост и неутешителен: никогда.

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

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

PS. Ну и несколько ссылок на свои старые заметки на эту тему:
Мои крамольные мысли о будущем языков программирования
О предсказании сроков написания программ
Опыт в программировании: переход от решения задачи к ее формализации.

среда, 19 ноября 2014 г.

[prog.c++.wow] Малообразованные C++ненависники доставляют

Не могу не утащить этот феерический фрагмент к себе (источник):

STL контейнеры были придуманы для приближения C++ к высотам и требованиям развитых высокоуровневых ЯП типа java/python/lisp и пр, с сохранением возможностей баловаться на низком C-шном уровне. Как известно, одной и то же жо...ой невозможно усидеть на двух противоположных поездах. Поэтому в C++ то крест спадает, то трусы сами надеваются. И программист, пишущий на C++, только и занят тем, что пытается одновременно и крест на шее удержать и трусы снять. Зрелище не очень так.

Особенно доставляет тот факт, что Степанов STL для C++ начал делать в начале 90-х годов. Т.е. тогда, когда Java не было в дикой природе, а Python делал лишь первые робкие шажочки. Что до Lisp-а, то первоначально свои идеи Степанов обкатывал на Scheme (т.е. на Lisp-е), затем на Ada. Но единственным языком, на котором Степанов, по его же признанию, смог воплотить в жизнь свои идеи полностью, оказался именно C++.

Ну а после появления STL уже сам C++ начал подстраиваться под STL и этот процесс взаимного влияния языка и STL-я, к счастью, продолжается до сих пор.

PS. Процитированный мной персонаж, smeeld, походу, сейчас является знаковой фигурой на RSDN-е (с вполне понятным направлением знака). Многие точки над ё вокруг этого товарища расставила вот эта тема (например, этот ее фрагмент).

[prog.flame] Масло маслянное?

Написал вчера вот такую строчку:

static const threading threading = THREADING;

Почему-то вспомнился термин тавтология ;)

Это я местами пытаюсь следовать стилю именования классов из C++/Boost, где не принято давать суффиксы _t именам нешаблонных классов. Если же вернуться к моему старому, проверенному годами, стилю, то было бы вот так:

static const threading_t threading = THREADING;

Тоже не образец изящного стиля, но для меня чуть понятнее.

Собственно, пока в C++ не начал широко использоваться STL, использование нотации с суффиксом _t для имен типов не вызывало особых сложностей. Но вот когда STL, а затем и Boost, пошел в массы, возникают штуки, когда суффикс не то, чтобы мешает, но выставляет наружу слишком много информации. Ну, например, если суффикс _t используется:

std::sort(a.begin(), a.end(), my_predicate_t(b, c, d));

tools::adaptor_t< reader_t, multithreading_t::safe, file_logger_t > adaptor;

Глядя на такой код программист понимает, что при вызове std::sort в качестве предиката создается экземпляр типа my_predicate_t. А в параметрах шаблона adaptor_t используются имена типов reader_t, multithreading_t, file_logger_t.

С одной стороны это хорошо. Но с другой, при сопровождении кода, это знание может только мешать. Например, если my_predicate должен стать вызовом возвращающей хитрый объект функции. Или, если со временем multithreading_t перестанет быть типом, а станет, скажем, пространством имен. А reader_t из типа превратится в константу. Если бы в коде было изначально записано вот так:

std::sort(a.begin(), a.end(), my_predicate(b, c, d));

tools::adaptor_t< reader, multithreading::safe, file_logger > adaptor;

То при сопровождении вообще менять бы ничего не пришлось.

На такие вещи я периодически натыкаюсь. И поэтому мой старый стиль именования типов мне иногда не нравится. А для имен типов, которые могут быть предикатами или параметрами шаблонов, стараюсь использовать имена без суффикса _t. Что терпимо, но некоторой шизофренией отдает: здесь _t используется, а вот здесь не используется, а вот здесь не понятно :(

С другой стороны, от суффикса _t оказываться так же не хочется. Ибо с ним можно запросто писать вот так:

threading_t threading = threading_t::single;
if( is_multicore_environment() )
   threading = threading_t::multi;

Не доводя дело до "масла масляного":

threading threading = threading::single;
if( is_multicore_environment() )
   threading = threading::multi;

PS. Как бы мелко эта тема не выглядела, она, тем не менее, затрагивает одну из фундаментальнейших и сложно разрешимых проблем в программировании: выбор удачных названий для идентификаторов ;)

PPS. CamelCase -- это пройденный этап. Кому интересно, отправляю в историю: "О том, как я от CamelCase к lower_case пришел".

вторник, 18 ноября 2014 г.

[prog.c++] И этот человек еще что-то говорит против трехэтажных шаблонов?! ;)

Я тут давеча высказался о том, что чрезмерное использование шаблонов в C++ слишком сильно перешагнуло границу добра и зла... Но сам, тем не менее, сварганил вот такую вот конструкцию:

templatetypename ENGINE >
class timer_manager_impl_template_t
   :  protected timer_manager_threading_dependent_part_t< ENGINE::threading > 
{
public :
   templatetypename... ARGS >
   timer_manager_impl_template_t( ARGS && ... args )
      :  m_engine( std::forward< ARGS >(args)... )
   {
   }
   ...
};

template<
   threading THREADING,
   typename ERROR_LOGGER,
   typename ACTOR_EXCEPTION_HANDLER >
class timer_list_manager_template_t
   : public details::timer_manager_impl_template_t<
               details::timer_list_engine_t<
                     THREADING,
                     ERROR_LOGGER,
                     ACTOR_EXCEPTION_HANDLER > > 
{
   ...
};

Что, почему-то, заставило вспомнить народное выражение "Чья бы корова мычала" :)))

PS. А вообще, конечно, C++11 рулит неимоверно. Жаль только, что какие-то фичи из С++11 в MS VisualStudio 2013 не поддерживаются. Например, наследование конструкторов. Но радует, что VisualStudio 2015 уже на подходе.

суббота, 15 ноября 2014 г.

[prog.c++.flame] Момент, когда хочется забросить программирование куда подальше

У меня бывало так: смотришь в чужой или в свой собственный, но старый и основательно забытый, код и на уровне интуиции понимаешь -- сильно сложно, могло было быть и должно было быть проще. А вот "математически выразить" это не можешь. Как правило, пути и способы упрощения кода находились, но не сходу, а после тщательного обдумывания (что заставляет вспомнить замечательную мудрость "простота не предшествует сложности, а следует за ней"). Тем не менее, сам этот момент, когда чувствуешь, что что-то не так, а что именно понять не можешь, имеет место быть.

Вот как сегодня, когда я решил посмотреть пост Tiny Metaprogramming Library в блоге Eric Niebler. Триггером послужил приведенный ниже код функции tuple_cat, которая принимает на вход набор туплов, а возвращает новый тупл, в котором последовательно перечислены значения из всех туплов-аргументов:

namespace detail
{
    template<typename Ret, typename...Is, typename ...Ks,
        typename Tuples>
    Ret tuple_cat_(typelist<Is...>, typelist<Ks...>,
        Tuples tpls)
    {
        return Ret{std::get<Ks::value>(
            std::get<Is::value>(tpls))...};
    }
}
 
template<typename...Tuples,
    typename Res =
        typelist_apply_t<
            meta_quote<std::tuple>,
            typelist_cat_t<typelist<as_typelist_t<Tuples>...> > > >
Res tuple_cat(Tuples &&... tpls)
{
    static constexpr std::size_t N = sizeof...(Tuples);
    // E.g. [0,0,0,2,2,2,3,3]
    using inner =
        typelist_cat_t<
            typelist_transform_t<
                typelist<as_typelist_t<Tuples>...>,
                typelist_transform_t<
                    as_typelist_t<make_index_sequence<N> >,
                    meta_quote<meta_always> >,
                meta_quote<typelist_transform_t> > >;
    // E.g. [0,1,2,0,1,2,0,1]
    using outer =
        typelist_cat_t<
            typelist_transform_t<
                typelist<as_typelist_t<Tuples>...>,
                meta_compose<
                    meta_quote<as_typelist_t>,
                    meta_quote_i<std::size_t, make_index_sequence>,
                    meta_quote<typelist_size_t> > > >;
    return detail::tuple_cat_<Res>(
        inner{},
        outer{},
        std::forward_as_tuple(std::forward<Tuples>(tpls)...));
}

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

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

Это типичный случай, когда интуитивно понимаешь, что слишком мудрено, что должно быть проще. А вот формализировать это впечатление не можешь, для этого нужно слишком много усилий. Разве что вспоминаются мои старые споры на RSDN-е с C++никами, которые считали, что Boost.Lambda, реализующая лямбда функции на шаблонах C++03, -- это нормально. И что пока встроенных язык лямбда-функций нет, вполне уместно пользоваться Boost.Lambda-ами. Если кто-то не понимает, о чем речь, то вот маленький пример из штатной документации к Boost.Lambda:

std::for_each(v.begin(), v.end(),
  ( 
    switch_statement(
      _1,
      case_statement<0>(std::cout << constant("zero")),
      case_statement<1>(std::cout << constant("one")),
      default_statement(cout << constant("other: ") << _1)
    ), 
    cout << constant("\n"
  )
);

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