В последние пару недель работаю с кодом, в котором активно используются циклические ссылки на динамически созданные объекты. В общих чертах там что-то вот такое:
namespace details { class data_t : public std::enable_shared_from_this { struct handler_t : public std::enable_shared_from_this { std::shared_ptr<data_t> owner_; ... }; std::vector<std::shared_ptr<handler_t>> handlers_; ... }; } /* namespace details */ template< typename Message > class definition_point_t { std::shared_ptr<details::data_t> data_; ... }; |
Т.е. есть объект типа definition_point_t
Это все означало, что ответственность за корректное уничтожение details::data_t лежит на тех типах, которые data_t используют. Это, в частности, тип definition_point_t. Соответственно, нужно было написать набор юнит-тестов, которые бы прогоняли различные сценарии использования data_t внутри definition_point_t, и проверяли, удаляется ли data_t в конце-концов или нет.
Проблема заключалась в том, что data_t создается внутри definition_point_t, хранится внутри definition_point_t как private-член и никакого доступа к нему снаружи нет. Ну и сам data_t делает только то, что от него требуется и не содержит внутри себя никаких трассировок или какого-то другого отладочного кода. Если кому-то интересно, как я выкрутился из этой ситуации, то милости прошу под кат.
Самый простой способ видится в том, чтобы директивами условной компиляции снабдить details::data_t нужными для тестов потрохами. Например, чем-то вроде:
class data_t : public std::enable_shared_from_this { struct handler_t : public std::enable_shared_from_this { std::shared_ptr<data_t> owner_; ... }; std::vector<std::shared_ptr<handler_t>> handlers_; ... #if defined(MY_UNIT_TEST_CASE) private: static std::atomic<int> live_objects_; public: static int live_objects() noexcept { return live_objects.load(std::memory_order_acquire); } private: #endif public: data_t(...) { #if defined(MY_UNIT_TEST_CASE) ++live_objects_; #endif } #if defined(MY_UNIT_TEST_CASE) ~data_t() { --live_objects; } #endif }; |
Но этот способ мне не нравится потому, что, во-первых, отладочные потроха торчат наружу. И, во-вторых, эти отладочные потроха заточены только под один сценарий тестирование -- контроль удаления объектов. Если бы мне захотелось протестировать еще что-нибудь, то пришлось бы добавлять еще что-то под другими директивами условной компиляции.
Следующий способ, который можно было бы рассмотреть -- это возможность создать своего наследника для details::data_t и затем передать в definition_point_t фабрику, которая должна использоваться для создания объектов details::data_t. Что-то вроде:
namespace details { class data_t : public std::enable_shared_from_this { struct handler_t : public std::enable_shared_from_this { std::shared_ptr<data_t> owner_; ... }; std::vector<std::shared_ptr<handler_t>> handlers_; ... }; using data_factory_t = std::shared_ptr<data_t>(*)(); std::shared_ptr<data_t> default_data_factory() { return std::make_shared<data_t>(); } } /* namespace details */ namespace my_test_cases { class definition_point_private_iface_t; } /* namespace my_test_cases */ template< typename Message > class definition_point_t { friend class my_test_cases::definition_point_private_iface_t; std::shared_ptr<details::data_t> data_; ... // Private constructor. Will be availiable only for definition_point_private_iface_t. definition_point_t( ..., // Some mandatory arguments. details::data_factory_t factory ) : data_(factory()) {...} public: // Public constructor. Will use the default factory. definition_point_t( ... /* Some mandatory arguments. */ ) : definition_point_t(..., details::default_data_factory) {} }; |
У definition_point_t появляется закрытый конструктор, который я смогу использовать в своих unit-тестах. Это позволит мне делать что-то вот такое:
namespace my_test_cases { class testing_data_t : public details::data_t { static std::atomic<int> live_objects_; public: ~testing_data_t() { --live_objects_; } static int live_objects() noexcept {...} static std::shared_ptr<data_t> factory() { return std::make_shared<testing_data_t>(); } }; definition_point_t<some_type> dp = definition_point_private_iface_t::create<some_type>( ..., // Some mandatory args. testing_data_t::factory); } /* namespace my_test_cases */ |
Т.е. создается наследник details::data_t с нужной мне функциональностью. Затем за счет использования специального закрытого интерфейса я могу создавать объекты definition_point_t подсовывая им свою фабрику. Эта фабрика вместо исходных details::data_t будет создавать testing_data_t. И уже в testing_data_t я могу делать то, что мне нужно для тестов.
Проблема в этом подходе состоит в том, что у details::data_t должен быть виртуальный деструктор. А по замыслу у details::data_t вообще не должно быть виртуальных методов и, соответственно, не должно быть таблицы виртуальных методов. Поэтому вариант с фабрикой не подходит.
И в итоге я остановился на том, чтобы добавить в шаблонный класс definition_point_t еще один параметр шаблона -- тип для data_t. Т.о. definition_point_t стал выглядеть как-то так:
template< typename Message, typename Data = details::data_t > class definition_point_t { std::shared_ptr<Data> data_; ... public: definition_point_t( ... /* Some mandatory arguments. */ ) : data_(std::make_shared<Data>()) ... {} ... }; |
И это дает мне возможность в unit-тестах создавать расширенные версии data_t:
namespace my_test_cases { class testing_data_t : public details::data_t { static std::atomic<int> live_objects_; public: ~testing_data_t() { --live_objects_; } static int live_objects() noexcept {...} }; definition_point_t<some_type, testing_data_t> dp(... /* Some mandatory args. */); REQUIRE(1 == testing_data_t::live_objects()); dp.do_something(); REQUIRE(0 == testing_data_t::live_objects()); } /* namespace my_test_cases */ |
И это работает без всяких виртуальных функций.
Конечно, не очень хорошо то, что пользователь видит дополнительный параметр шаблона для definition_point_t. Но т.к. это параметр со значением по умолчанию, пользователю не приходится иметь с этим дела. А то, что у definition_point_t появился еще один шаблонный параметр погоды принципиально не меняет, все равно это шаблонный класс.
PS. На самом деле в моем коде использовался специальный класс интрузивного умного указателя, а не std::shared_ptr, поэтому трюк с тем, что shared_ptr может корректно удалить класс наследника даже без виртуального деструктора, у меня бы не прошел. Класс std::shared_ptr использовался здесь лишь для простоты восприятия.
Комментариев нет:
Отправить комментарий