суббота, 31 октября 2015 г.

[prog.c++11] Автоматический возврат объекта в пул за счет кастомного deleter-а и стандартных умных указателей

Никогда раньше с таким применением умных указателей не сталкивался, но забавная тема на LOR-е показала, что стандартные умные указатели легко могут использоваться не только для уничтожения объекта, но и для возврата объекта в тот пул объектов, из которого объект был взят.

Суть проблемы: есть пул объектов типа T, объект берется на какое-то время из пула, а когда объект становится больше не нужен, он должен вернуться обратно в пул. Причем пулов в программе может быть много. А код, который использует взятый из пула объект, ничего про пул не знает, но объект должен вернуться в тот пул, которому принадлежит. При этом гарантируется, что пулы живут долго, так что о той проблеме, что пул будет разрушен раньше, чем в него вернут все взятые ранее объекты, можно не беспокоится.

Решение на удивление простое и красивое. Оно базируется на том факте, что для стандартных умных указателей (std::unque_ptr и std::shared_ptr) можно задавать кастомный deleter. Для данной задачи с пулом можно сделать deleter, который будет хранить в себе указатель на пул из которого был взят объект. А когда дело доходит до вызова этого deleter-а (т.е. когда разрушается unique_ptr или последний shared_ptr), то кастомный deleter вместо физического уничтожения объекта просто возвращает его в пул.

Вот набросок простого пула объекта, который работает именно по такой схеме:

templatetypename 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>

templatetypename 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 );
   }

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