вторник, 10 июня 2014 г.

[prog.c++] Ну вот нравятся мне возможности современного C++!

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

Тем не менее, время шло, долгострой под названием C++0x успешно завершился и его результаты (например, в Visual C++ 2012/2013 или GCC 4.8/4.9) не могут не радовать. Например, пару дней назад перерабатывал старый класс, в нескольких методах которого нужно было простым поиском найти элемент в векторе простых структур. Поскольку операция эта 1-в-1 повторялась во всех методах, то возникла мысль вынести обращение к std::find_if в один вспомогательный метод, а дальше дергать только его. Но...

Но есть нюанс :) В C++ не получится ограничится вот такой простой реализацией:

typedef std::vector< state_and_handler_t > handler_container_t;

handler_container_t::iterator
try_find_handler( handler_container_t & where,
      const state_t & search_key )
   {
      return std::find_if( where.begin(), where.end(), ... );
   }

Поскольку, если экземпляр handler_container_t является атрибутом какого-то объекта, то иногда ссылка на него будет константной:

class handler_caller_t
   {
      handler_container_t m_handlers;
      ...
   public :
      void call_handler( const state_t & current_state ) const
         {
            // Это не скомпилируется!!!
            handler_container_t::iterator it = try_find_handler(
                  m_handlers, current_state );
            ...
         }

      void remove_handler( const state_t & target_state )
         {
            m_handlers.erase(
                  try_find_handler( m_handlers, target_state ) );
         }
      ...
   };

Проблема здесь в том, что в handler_caller_t::call() ссылка на m_handlers константна, т.к. сам метод call() константен. Поэтому ее нельзя передать в try_find_handler.

При работе в C++03 на ум сразу приходит два решения: либо тупо продублировать try_find_handler для константного и неконстантного аргументов, либо же попробовать сделать try_find_handler шаблоном, тип аргумента которого выводится. Второй вариант кажется более привлекательным, т.к. он позволяет избежать дублирования кода, но он не сработает:

templateclass C >
typename C::iterator
try_find_handler( C & where,
      const state_t & search_key )
   {
      return std::find_if( where.begin(), where.end(), ... );
   }

Проблема в возвращаемом значении. Его тип будет iterator только для неконстантного контейнера. Если же в try_find_handler передается константная ссылка, то методы begin()/end() будут возвращать const_iterator, а не iterator. Следовательно, const_iterator будет возвращаться из std::find_if. А так как const_iterator -- это отличный от типа iterator тип, то значение данного типа нельзя вернуть из такого варианта try_find_handler. Что приведет к невозможности скомпилировать шаблонный вариант try_find_handler для константного контейнера.

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

К счастью, сейчас я могу себе позволить ограничится только C++11. А в нем, за счет некоторых новых фич, появляется возможность написать шаблонный код try_find_handler всего один раз. Вот как это в итоге стало выглядеть у меня:

templateclass C >
auto /* (1) */
try_find_handler(
   C & container,
   const state_t & state ) -> decltype( std::begin(container) ) /* (2) */
   {
      typedef decltype(*std::begin(container)) value_t; /* (3) */

      return std::find_if(
            std::begin( container ),
            std::end( container ),
            [&state]( value_t & o ) { return o.m_state == &state; } );
   }

Ключевые моменты помеченны в комментариях циферьками.

Пункт первый: в С++11 можно не писать сразу тип возвращаемого функцией/методом значения, а вместо этого поставить ключевое слово auto. Это означает, что тип возвращаемого значения будет уточнен после декларации аргументов функции. Что позволяет задействовать типы аргументов в процессе вывода типа возвращаемого значения.

Именно это показано в пункте (2). Там за счет еще одной новой фишки C++11 -- decltype -- определяется, какой тип возвращает std::begin() для данного типа контейнера. Не суть важно, будет ли это const_iterator, iterator или что-то еще. Важно, что компилятор определит это сам, и будет считать, что значение именно этого типа будет возвращаться самой функцией try_find_handler().

Пункт три, хоть и показывает еще раз возможности C++11, на самом деле нужен для того, чтобы обойти ограничение, которое еще есть в C++11, но которого не будет в C++14. Дело в том, что в std::find_if передается лямбда-функция. А в ее декларации нужно указать, какой тип будет у ее аргумента. В C++11 это нужно сделать явно, а вот в C++14 можно будет указать вместо типа auto и тип аргумента выведет компилятор.

В данном случае, поскольку я имею дело с собственным контейнером, можно было бы тупо указать тип его элемента, т.к. я все равно его знаю. Но хотелось задействовать C++11 на полную катушку и пойти максимально далеко :) Да, такой overuse для полезной feature, но получилось интересно :)

Так вот, в пункте (3) за счет decltype определяется, какой тип будет, если разыменовать возвращенный std::begin() итератор. Т.е. тип значения, на которое ссылается итератор. Этому типу через typedef дается псевдоним value_t, а затем value_t используется в декларации лямбда-функции.

Еще один маленький, а может и не маленький, бонус мы получаем в связи с использованием std::begin/std::end вместо std::vector::begin/end. Это позволяет нам использовать в качестве контейнера обычные C-шные массивы, у которых нет методов begin/end. Но зато с ними прекрасно работают свободные функции std::begin/end.

Полный текст автономного примера, демонстрирующего try_find_handler для разных типов контейнеров, приведен под катом.

Еще раз отмечу, что C++11, на мой взгляд, настолько далеко продвинул C++ вперед, что сейчас он воспринимается, если не как совсем новый язык, то уж как очень сильно усовершенствованный язык точно. Конечно, утечки памяти, обращения по невалидным указателям и прочие прелести native-разработки никуда не делись, хотя за счет усовершенствования стандартной библиотеки количество граблей несколько уменьшилось. Но общее удовольствие от работы на C++ сейчас гораздо выше. В чем-то даже похоже на удовольствие, которое я когда-то получал от программирования на Ruby. Только при этом язык позволяет писать очень большие и намного более эффективно работающие программы :)

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

Раньше C++ные возможности пытались эксплуатировать для того, чтобы получить что-то, чего в языке не было. Навороченные шаблоны имени Александреску, Вандервуда или Джосаттиса тому пример (особенно мне нравится в качестве примера приводить старую Boost.Lambda). Поэтому и прикладной код получался не слишком понятным, а уж во вспомогательные библиотеки и вовсе страшно было заглядывать.

Теперь же C++ настолько продвинулся, что сложность его восприятия возникает из-за его выскоуровневости (которая, при этом, в умелых руках не противоречит эффективности). Так, рассматривая некоторые примеры кода на современном C++ возникает чувство, что читаешь какой-то Haskell-ный код, который чуть-чуть больше разбавлен анотациями и в котором используются нормальные, а не однобуквенные с апострофами, идентификаторы :) Вот, например, коротенький пример из отличной статьи Бьёрна Страуструпа "Software Development for Infrastructure":

template <typename Iter, typename Predicate>
pair<Iter, Iter>
gather(Iter first, Iter last, Iter p, Predicate pred)
   // move e for which pred(e) to the insertion point p
   {
      return make_pair(
            // from before insertion point:
            stable_partition(first, p, !bind(pred, _1)),
            // from after insertion point:
            stable_partition(p, last, bind(pred, _1))
         );
   }

Статья, кстати говоря, классная. Рекомендую, даже не смотря на то, что она объемная и на английском. Интересные вопросы Страуструп там затрагивает, многие из которых касаются не только C++. И, если Страуструп прав, а мне кажется, что во многом он прав, то будущее С++ представляется уже не таким мрачным, как 10 лет назад. Особенно, если C++14 и C++17 не станут такими же долгостроями, как C++0x.

Ну а под катом полный текст автономного примера, с которым можно поиграться.

PS. Еще пара заметок на тему современного C++ и функционального стиля: #1, #2.

#include <vector>
#include <list>
#include <algorithm>

struct state_t {};

struct handler_t {};

struct state_and_action_t {
   state_t * m_state;
   handler_t * m_handler;

   state_and_action_t(
      state_t * state = nullptr,
      handler_t * handler = nullptr )
      :  m_state( state )
      ,  m_handler( handler )
      {}
};

templateclass C >
auto /* (1) */
try_find_handler(
   C & container,
   const state_t & state ) -> decltype( std::begin(container) ) /* (2) */
   {
      typedef decltype(*std::begin(container)) value_t; /* (3) */

      return std::find_if(
            std::begin( container ),
            std::end( container ),
            [&state]( value_t & o ) { return o.m_state == &state; } );
   }

templateclass C >
void
test( C & container )
   {
      state_t value_to_find;

      try_find_handler( container, value_to_find );
   }

int
main()
   {
      typedef std::vector< state_and_action_t > vector_t;
      vector_t v;
      const vector_t & const_v = v;

      test( v );
      test( const_v );

      typedef std::list< state_and_action_t > list_t;
      list_t l;
      const list_t const_l = l;

      test( l );
      test( const_l );

      state_and_action_t a[ 3 ];

      test( a );
   }

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