пятница, 6 декабря 2013 г.

[prog.c++] Пример сохранения ссылок на внешние объекты внутри класса

Эта заметка, как и одна из предыдущих, является иллюстрацией к моим аргументам в большой дискуссии на 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, можно пользоваться и вот таким вот приемом.

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