Программирование на современном 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 шаблоном, тип аргумента которого выводится. Второй вариант кажется более привлекательным, т.к. он позволяет избежать дублирования кода, но он не сработает:
template< class 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 всего один раз. Вот как это в итоге стало выглядеть у меня:
template< class 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 ) {} }; template< class 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; } ); } template< class 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 ); } |
Комментариев нет:
Отправить комментарий