Эта заметка, как и одна из предыдущих, является иллюстрацией к моим аргументам в большой дискуссии на LOR-е. Очевидно, что у некоторых C++ разработчиков есть предубеждение против сохранения внутри объекта ссылок на внешние объекты. В какой-то степени эти предубеждения оправданы. Но дело, однако, в том, что в таком хитром языке, как C++, нет простых способов обеспечения валидности сохраненной в объекте ссылки/указателя на внешний объект. Даже std::unique_ptr и std::shared_ptr не дают никаких гарантий: объект может быть выделен из пула (возвращается в пул кастомным deleter-ом) и тогда не смотря на unique_ptr/shared_ptr объект может исчезнуть из-за уничтожения пула. Так что бдить приходится всегда. А когда бдишь уже не так страшно хранить в объекте ссылки, а не умные указатели или, уж тем более, голые указатели. Об этом и пойдет речь ниже.
Поскольку это тема специфическая и содержит примеры кода, то она упрятана под кат. Кому интересно, милости прощу.
Так же считаю нужным отметить, что примеры эти я привожу не для демонстрации собственной крутости, незаурядности или, как меня недавно обвинили, илитарности. С одной стороны, это очень простые вещи, до которых, почему-то, я дошел далеко не сразу. И, может быть, они помогут еще кому-нибудь продвинутся чуть дальше. С другой стороны, зачастую публикуя фрагменты кода получаешь очень полезную обратную связь, благодаря которой начинаешь разбираться в языке или даже вообще в программировании чуть лучше. Вот ради этого я и разбираю примеры собственного кода. Ну а после такого disclaimer-а можно перейти непосредственно к слайдам.
В очередную версию SObjectizer-а добавлена новая (т.е. хорошо забытая старая) фича: поддержка взаимоотношений родитель-потомок для коопераций. Чем-то это напоминает иерархии процессов в Erlang-е, но отдаленно. Суть данной фичи в том, что когда кооперация-родитель создает одну или несколько коопераций-потомков, то дерегистрация (т.е. удаление) родительской кооперации автоматически должна привести к дерегистрации (удалению) всех ее дочерних коопераций (а так же дочерних коопераций дочерних коопераций и т.д.).
В процессе добавления данной фичи возникла потребность переписывания вот такого метода:
void agent_core_t::deregister_coop( const nonempty_name_t & name ) { const std::string & coop_name = name.query_name(); agent_coop_ref_t coop; { // All the following actions should be taken under the lock. ACE_Guard< ACE_Thread_Mutex > lock( m_coop_operations_lock ); // Noone action if cooperation is already // in the deregistration process. if( m_deregistered_coop.end() != m_deregistered_coop.find( coop_name ) ) { return; } // It is an error if the cooperation is not registered. coop_map_t::iterator it = m_registered_coop.find( coop_name ); if( m_registered_coop.end() == it ) { SO_5_THROW_EXCEPTION( rc_coop_has_not_found_among_registered_coop, "coop with name \"" + coop_name + "\" not found among registered cooperations" ); } coop = it->second; m_registered_coop.erase( it ); m_deregistered_coop[ coop_name ] = coop; } // All agents of the cooperation should be marked as deregistered. coop->undefine_all_agents(); } |
А именно, нужно было разбить процесс дерегистрации на две стадии. На первой стадии должен быть собран набор имен всех подлежащих дерегистрации коопераций с соответствующей модификаций служебных словарей. На второй стадии требовалось указать каждой из найденных на первой стадии коопераций на необходимость завершения работы. Плюс, попутно, хотелось устранить некоторые претензии к старому коду по поводу обеспечения exception safety.
Первая попытка выразить все эти операции с помощью методов класса agent_core_t, которому принадлежит метод deregister_coop_t, привела к тому, что в agent_core_t нужно было бы добавить ряд методов, которые бы "раздули" класс agent_core_t. И не только бы раздули, но еще и усложнили бы обработку исключений на определенных стадиях процесса дерегистрации. Так же, при работе этих методов нужно было передавать некоторые данные, которые либо утяжелили бы прототипы методов (каждый метод бы получал и передавал дальше кучу параметров). Либо же эти данные нужно было оформлять в какую-то вспомогательную структуру. И раз уж эта вспомогательная структура замаячила на горизонте, я вспомнил прием, которому научился знакомясь с языком Eiffel.
В идеологии Eiffel есть такая красивая, хотя и неоднозначная вещь, как разделение методов класса на command- и query-методы (т.н. command/query separation principle). Command-методы изменяют состояние объекта, но не возвращают результата. Query-методы (или функции), позволяют опрашивать состояние объекта, но не меняют его (в этом query-методы очень похожи на функции в смысле функционального программирования -- последовательный вызов функций всегда должен приводить к одному и тому же результату). Хотя для ОО-языка этот принцип далеко не однозначен (подробнее здесь), но в Eiffel-е зачастую нет выбора: чтобы сделать какую-то модификацию, а затем узнать ее результат, приходится делать небольшой класс, который сначала делает эту модификацию посредством command-методов, а результат операции возвращает query-методом. Писанины это добавляет изрядно, но, как ни странно, делает код более понятным и сопровождаемым.
Этим приемом, т.е. созданием вспомогательного класса, отвечающего за выполнение всего одной операции, я и решил в данном случае воспользоваться. Получившийся класс содержал всего один публичный command-метод, но это не суть важно. Важно, что с его помощью код agent_core_t::deregister_coop стал выглядеть так:
void agent_core_t::deregister_coop( const nonempty_name_t & name ) { agent_core_details::deregistration_processor_t processor( *this, name.query_name() ); processor.process(); } |
В контексте данного разговора важно, что в конструктор класса deregistration_processor_t два нужных ему параметра передаются по ссылке! Сохраняются эти параметры внутри deregistration_processor_t так же по ссылке:
class deregistration_processor_t { public : //! Constructor. deregistration_processor_t( //! Owner of all data. agent_core_t & core, //! Name of root cooperation to be deregistered. const std::string & root_coop_name ); //! Do all necessary actions. void process(); private : //! Owner of all data to be handled. agent_core_t & m_core; //! Name of root cooperation to be deregistered. const std::string & m_root_coop_name; |
И в этом нет никакого криминала, т.к. контекст разрешенного целевого использования экземпляров класса deregistration_processor_t гарантирует то, что данные ссылки не могут просто так стать невалидными. Конечно, в будущем чьи-нибудь шаловливые ручонки могут довести до маразма изменить ситуацию до неузнаваемости, но очень уж тяжело, а потому и не хочется, писать защищенный от дурака код. Тем более, когда речь идет о библиотеке, разрабатываемой с прицелом на достижение в будущем высокой производительности и эффективности.
В общем, мораль такова: когда разработчик знает что и зачем он делает, понимает, какие у него цели и ограничения, использование ссылок в качестве полей объектов со строго регламентированным by-design временем жизни, вполне нормальная практика.
PS. Полный код класса deregistration_processor_t можно найти в этом файле. Данный класс, хоть он и самостоятельный, я лично рассматриваю как private-часть класса agent_core_t. В таких языках, как Modula-2, Ada и Turbo Pascal, у модулей можно было явно описывать приватную часть модулей, скрытую от посторонних. В C++ какой-то аналогией модулей являются классы (в а Eiffel прямой аналогией). Но, к сожалению, приватная часть C++ класса должна декларироваться в том же месте, где и публичная часть. Если же раздувать приватную часть C++класса не хочется, то кроме идиомы PImpl, можно пользоваться и вот таким вот приемом.
Комментариев нет:
Отправить комментарий