Язык C++ никогда не был простым языком. Это объективно и проистекает как из специализации языка, так и из истории его создания и развития. Однако, в последние годы лично я твердо уверен в том, что поддерживаемая в интернетах репутация C++ как очень сложного в изучении и использовании языка, базируется, в первую очередь, на сильно устаревшей информации (времен отсутствия стандарта языка, а так же времен отсутствия компиляторов с более-менее нормальной поддержкой C++98/03). А во вторую очередь на фольклоре, распространяемом теми, кто ни самого языка, ни систем на нем, не видел вообще.
C++11/14 стал еще более сложным, чем C++03. Но фокус в том, что писать на нем стало намного проще. Т.е. усложнение языка сильно упростило разработку на C++. Поэтому не нужно к C++11/14 применять ярлыки, заслуженные много лет назад C++98 (а то и более ранними версиями). Под катом пара примеров реального кода для демонстрации данного тезиса.
Первый пример -- преобразование содержимого двух контейнеров (std::map и std::unordered_map) в контейнер типа вектор (std::vector). Это преобразование выполняется с целью экспорта информации из некоторого хранилища наружу:
subscription_storage_common::subscr_info_vector_t storage_t::query_content() const { using namespace std; using namespace subscription_storage_common; subscr_info_vector_t events; if( !m_hash_table.empty() ) { events.reserve( m_hash_table.size() ); transform( begin(m_hash_table), end(m_hash_table), back_inserter(events), [this]( const hash_table_t::value_type & i ) { auto map_item = m_map.find( *(i.first) ); return subscr_info_t { map_item->second, map_item->first.m_msg_type, *(map_item->first.m_state), i.second.m_method, i.second.m_thread_safety }; } ); } return events; } |
Этот код не сложен. Он безопасен (в том числе и по типам, соответствие которых проверяется компилятором). Он эффективен (в векторе реализуется move semantic, поэтому возврат вектора из метода не приводит к поэлементному копированию его содержимого). В нем нет ручного управления памятью, а времена жизни всех объектов очевидны и детерминированы. Более того, это, фактически, написанный в ФП-стиле код, ведь transform -- это чистой воды операция map, а все действия выполняются внутри const-метода, что не дает просто так модифицировать состояние объекта, для которого вызывается query_content. И, что еще немаловажно, этот метод exception safe: при возникновении исключения ничего не испортится, а все, что уже было создано до исключения, будет автоматически подчищено.
Похоже это на старый-добрый C++ до-STL-евских времен, когда не было std::vector и нужно было использовать вручную размещенные в динамической памяти C-ные вектора? Или даже C++ времен 03-го стандарта, когда для обеспечения эффективности результирующий std::vector нужно было бы передавать в query_content параметром по ссылке, а затем думать про его очистку при выбрасывании исключения?
А вот второй пример, который как раз непосредственно связан с обеспечением exception safety:
void agent_core_t::next_coop_reg_step__update_registered_coop_map( const agent_coop_ref_t & coop_ref, agent_coop_t * parent_coop_ptr ) { m_registered_coop[ coop_ref->query_coop_name() ] = coop_ref; m_total_agent_count += coop_ref->query_agent_count(); // In case of error cooperation info should be removed // from m_registered_coop. so_5::details::do_with_rollback_on_exception( [&] { next_coop_reg_step__parent_child_relation( coop_ref, parent_coop_ptr ); }, [&] { m_total_agent_count -= coop_ref->query_agent_count(); m_registered_coop.erase( coop_ref->query_coop_name() ); } ); } |
Это один из шагов выполнения многофазовой операции. На каждом из шагов нужно выполнить ряд действий по модификации состояния. Часть из этих действий может привести к возникновению исключения (например, из-за нехватки памяти или нарушения каких-то инвариантов). При возникновении исключения на очередном шаге нужно откатить изменения, которые были сделаны ранее.
В показанном примере исключения могут возникнуть в двух местах:
- при добавлении нового элемента в контейнер m_registered_coop. Если здесь возникнет исключение, никаких отмен внутри метода next_coop_reg_step__update_registered_coop_map выполнять не нужно, т.к. еще ничего не модифицировано. Поэтому возникающие здесь исключения выпускаются наружу и пусть с ними разбираются где-то наверху;
- при выполнении действий на следующей фазе, т.е. внутри метода next_coop_reg_step__parent_child_relation. Если оттуда вылетит исключение, то нужно будет отменить действия, которые были выполнены ранее на этом шаге (счетчик m_total_agent_count должен быть уменьшен, новый элемент из m_registered_coop должен быть удален).
В старом-добром C++98/03 для обеспечения exception safety потребовалось бы либо написать парочку try-catch, либо же определить вспомогательный класс, в деструкторе которого проводилась бы отмена выполненых на этом шаге операций. Вариант со вспомогательным классом, на мой взгляд, был бы более предпочтительным, т.к. в этом случае автоматически обеспечивается очень простая реакция на исключения, выбрасываемые при раскрутке стека из-за предыдущего исключения.
Но вспомогательный класс -- это дополнительный код, который нужно написать, а потом сопровождать. Конечно, это дело можно было бы чуть упростить за счет шаблонной и макросовой магии (см. BOOST_SCOPE_EXIT), но это именно что использование магии, которая как раз и портит карму.
В С++11 ситуация ну очень простая -- вспомогательная шаблонная функция, получающая две лямбды. Первая выполняет нужные действия, вторая дергается при выбрасывании исключения из первой. Что упрощает написание exception safe кода на порядок. И все это с использованием только штатных средств, с соответствующей проверкой типов и т.д. Да и за эффективность переживать особо не стоит, т.к. все это на шаблонах, то в подавлающем большинстве случаев код этих лямбд будет просто-напросто встроен прямо по месту их использования.
Желающие могут попробовать представить, как такой же код мог бы выглядеть в Java или C# (где есть исключения, но нет детерминированного вызова деструкторов). Или в Go или Rust-е, где нет исключений, а нужно проверять коды ошибок. Что-то мне не кажется, что аналогичный код был бы компактнее, понятнее и проще в сопровождении.
В реализации do_with_rollback_on_exception нет совсем никакой магии, только штатные возможности языка C++11. Там единственный фокус -- специализированная версия шаблона executor для случая, когда первая лямбда возвращает void. Все остальное тривиально. Для того, чтобы написать такой код или разобраться в нем не нужно быть экспертом по шаблонному метапрограммированию, как это требовалось еще лет пять назад. Достаточно просто иметь представление о C++ных шаблонах.
А в 2015-ом году о C++ных шаблонах обязательно нужно иметь представление. Иначе смысла работать с C++ нет, вам вполне достаточно будет обычного plain old C, со всей его прямолинейностью и убогостью простотой :)
Комментариев нет:
Отправить комментарий