Никогда раньше с таким применением умных указателей не сталкивался, но забавная тема на LOR-е показала, что стандартные умные указатели легко могут использоваться не только для уничтожения объекта, но и для возврата объекта в тот пул объектов, из которого объект был взят.
Суть проблемы: есть пул объектов типа T, объект берется на какое-то время из пула, а когда объект становится больше не нужен, он должен вернуться обратно в пул. Причем пулов в программе может быть много. А код, который использует взятый из пула объект, ничего про пул не знает, но объект должен вернуться в тот пул, которому принадлежит. При этом гарантируется, что пулы живут долго, так что о той проблеме, что пул будет разрушен раньше, чем в него вернут все взятые ранее объекты, можно не беспокоится.
Решение на удивление простое и красивое. Оно базируется на том факте, что для стандартных умных указателей (std::unque_ptr и std::shared_ptr) можно задавать кастомный deleter. Для данной задачи с пулом можно сделать deleter, который будет хранить в себе указатель на пул из которого был взят объект. А когда дело доходит до вызова этого deleter-а (т.е. когда разрушается unique_ptr или последний shared_ptr), то кастомный deleter вместо физического уничтожения объекта просто возвращает его в пул.
Вот набросок простого пула объекта, который работает именно по такой схеме:
template< typename T > class object_pool { using uniq_pointer = std::unique_ptr< T >; std::deque< uniq_pointer > m_objects; friend struct deleter; struct deleter { object_pool * m_pool; void operator()( T * o ) { m_pool->put_back( o ); } }; void put_back( T * o ) { std::cout << this << ": put_back=" << *o << std::endl; m_objects.push_back( uniq_pointer{ o } ); } public : object_pool() {} object_pool( const object_pool & ) = delete; object_pool( object_pool && ) = delete; void fill( uniq_pointer o ) { m_objects.push_back( std::move(o) ); } std::unique_ptr< T, deleter > get() { if( m_objects.empty() ) throw std::runtime_error( "empty_pool" ); std::unique_ptr< T, deleter > o{ m_objects.front().release(), deleter{ this } }; m_objects.pop_front(); std::cout << this << ": get=" << *o << std::endl; return o; } }; |
Видно, что object_pool::get() возвращает не простой unqiue_ptr<T>, а более хитрый, со своим deleter-ом. Именно этот deleter и возвращает затем объект обратно в пул.
При реализации object_pool::get(), в принципе, можно выбирать, какой тип возвращать: либо unique_ptr<T,deleter>, либо shared_ptr<T>. Если возвращается shared_ptr, то получается вообще непрозрачная для пользователя схема работы: пользователь даже не знает, что shared_ptr будет возвращать объект в пул, а не удалять его физически. Происходит это потому, что тип deleter-а известен только на этапе конструирования shared_ptr-а, тогда как в случае с unique_ptr-ом тип deleter-а явным образом входит в описание типа unique_ptr-а.
Так же возврат shared_ptr<T> привлекателен тем, что позволяет записать object_pool::get() с использованием лямбд, без необходимости определять struct deleter:
std::shared_ptr< T > get() { if( m_objects.empty() ) throw std::runtime_error( "empty_pool" ); uniq_pointer o{ std::move(m_objects.front()) }; m_objects.pop_front(); std::cout << this << ": get=" << o.get() << std::endl; return std::shared_ptr< T >( o.release(), [this]( T * o ) { std::cout << this << ": put_back=" << o << std::endl; this->m_objects.push_back( uniq_pointer{ o } ); } ); } |
Но лично мне кажется, что возврат unique_ptr<T,deleter> все-таки более предпочтителен. Т.к. для простых сценариев использования взятого из пула объекта это наиболее дешевый вариант. А если нужны более сложные случаи, когда требуется shared_ptr, то unqiue_ptr с кастомным deleter-ом автоматически трансформируется в shared_ptr с тем же самым кастомным deleter-ом. Поэтому ниже приводится простая тестовая програмка, на которой простейший object_pool и проверялся. Объект из пула возвращается в виде unique_ptr, но при использовании, так где это требуется, преобразуется к unique_ptr:
#include <iostream> #include <memory> #include <deque> #include <string> template< typename T > class object_pool { using uniq_pointer = std::unique_ptr< T >; std::deque< uniq_pointer > m_objects; friend struct deleter; struct deleter { object_pool * m_pool; void operator()( T * o ) { m_pool->put_back( o ); } }; void put_back( T * o ) { std::cout << this << ": put_back=" << *o << std::endl; m_objects.push_back( uniq_pointer{ o } ); } public : object_pool() {} object_pool( const object_pool & ) = delete; object_pool( object_pool && ) = delete; void fill( uniq_pointer o ) { m_objects.push_back( std::move(o) ); } std::unique_ptr< T, deleter > get() { if( m_objects.empty() ) throw std::runtime_error( "empty_pool" ); std::unique_ptr< T, deleter > o{ m_objects.front().release(), deleter{ this } }; m_objects.pop_front(); std::cout << this << ": get=" << *o << std::endl; return o; } }; class my_object; std::ostream & operator<<( std::ostream & to, const my_object & o ); class my_object { std::string m_name; public : my_object( std::string name ) : m_name{ std::move(name) } { std::cout << *this << " - created" << std::endl; } ~my_object() { std::cout << *this << " - destroyed" << std::endl; } my_object( const my_object & ) = delete; my_object( my_object && ) = delete; const std::string & name() const { return m_name; } }; std::ostream & operator<<( std::ostream & to, const my_object & o ) { return (to << "{" << &o << "=" << o.name() << "}" ); } using my_object_shptr = std::shared_ptr< my_object >; void use_1( my_object_shptr obj ) { std::cout << "use_1: " << *obj << std::endl; } void use_2( my_object_shptr obj ) { std::cout << "use_2: " << *obj << std::endl; } void demo( object_pool< my_object > & pool ) { my_object_shptr o1{ pool.get() }; use_1( o1 ); { my_object_shptr o2{ pool.get() }; use_1( o2 ); use_2( o2 ); { my_object_shptr o3{ pool.get() }; use_1( o3 ); use_2( o3 ); } { my_object_shptr o3{ pool.get() }; use_1( o3 ); use_2( o3 ); } } { my_object_shptr o2_1{ pool.get() }; my_object_shptr o2_2{ pool.get() }; use_1( o2_1 ); use_1( o2_2 ); } { auto o3{ pool.get() }; std::cout << "unique_ptr simple case: " << *o3 << std::endl; } } int main() { object_pool< my_object > pool; pool.fill( std::make_unique< my_object >( "First" ) ); pool.fill( std::make_unique< my_object >( "Second" ) ); pool.fill( std::make_unique< my_object >( "Third" ) ); demo( pool ); } |
Комментариев нет:
Отправить комментарий