Почему работают std:: shared ptr


Я нашел некоторый код, используя std:: shared_ptr для выполнения произвольной очистки при выключении. Сначала я думал, что этот код не может работать, но потом я попробовал следующее:

#include <memory>
#include <iostream>
#include <vector>

class test {
public:
  test() {
    std::cout << "Test created" << std::endl;
  }
  ~test() {
    std::cout << "Test destroyed" << std::endl;
  }
};

int main() {
  std::cout << "At begin of main.ncreating std::vector<std::shared_ptr<void>>" 
            << std::endl;
  std::vector<std::shared_ptr<void>> v;
  {
    std::cout << "Creating test" << std::endl;
    v.push_back( std::shared_ptr<test>( new test() ) );
    std::cout << "Leaving scope" << std::endl;
  }
  std::cout << "Leaving main" << std::endl;
  return 0;
}

эта программа дает на выходе:

At begin of main.
creating std::vector<std::shared_ptr<void>>
Creating test
Test created
Leaving scope
Leaving main
Test destroyed

у меня есть некоторые идеи о том, почему это может работать, которые имеют отношение к внутренним элементам std::shared_ptrs, реализованным для G++. Так как эти объекты обертывают внутренний указатель вместе со счетчиком приведение от std::shared_ptr<test> до std::shared_ptr<void> - это вероятно, не препятствуя вызову деструктора. Правильно ли это предположение?

и, конечно, гораздо более важный вопрос: гарантируется ли это работа по стандарту или могут ли дальнейшие изменения во внутренних компонентах std:: shared_ptr, другие реализации фактически нарушают этот код?

6 117

6 ответов:

фишка в том, что std::shared_ptr выполняет стирания типа. В принципе, когда новый shared_ptr создается он будет хранить внутренне a deleter функция (которая может быть задана в качестве аргумента конструктору, но если нет, по умолчанию вызывается delete). Когда shared_ptr уничтожается, он вызывает эту сохраненную функцию, и это вызовет deleter.

простой эскиз стирания типа, который упрощается с помощью функции std::и избегает всех ссылок подсчет и другие вопросы можно посмотреть здесь:

template <typename T>
void delete_deleter( void * p ) {
   delete static_cast<T*>(p);
}

template <typename T>
class my_unique_ptr {
  std::function< void (void*) > deleter;
  T * p;
  template <typename U>
  my_unique_ptr( U * p, std::function< void(void*) > deleter = &delete_deleter<U> ) 
     : p(p), deleter(deleter) 
  {}
  ~my_unique_ptr() {
     deleter( p );   
  }
};

int main() {
   my_unique_ptr<void> p( new double ); // deleter == &delete_deleter<double>
}
// ~my_unique_ptr calls delete_deleter<double>(p)

Когда a shared_ptr копируется (или по умолчанию строится) из другого deleter передается вокруг, так что при построении shared_ptr<T> С shared_ptr<U> информация о том, какой деструктор вызывать, также передается в deleter.

shared_ptr<T> логически [*] имеет (по крайней мере) два соответствующих члена данных:

  • указатель на управляемый объект
  • указатель на функцию удаления, которая будет использоваться, чтобы уничтожить его.

функция deleter вашего shared_ptr<Test>, учитывая, как вы его построили, является нормальным для Test, который преобразует указатель на Test* и deletes это.

когда вы нажимаете ваш shared_ptr<Test> в вектор shared_ptr<void>, и из них копируются, хотя первый преобразуется в void*.

Итак, когда векторный элемент уничтожается, беря с собой последнюю ссылку, он передает указатель на делетер, который правильно его уничтожает.

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

[*] логически в том смысле, что он имеет к ним доступ - они могут быть не членами самого shared_ptr, а вместо некоторого узла управления, на который он указывает.

он работает, потому что он использует стирание типа.

в основном, когда вы строите shared_ptr, Он передает один дополнительный аргумент (который вы можете предоставить, если хотите), который является функтором deleter.

этот функтор по умолчанию принимает в качестве аргумента указатель на тип вы используете в ,void здесь, приводит его соответствующим образом к статическому типу, который вы использовали test вот, и вызывает деструктор на этот объект.

Любая достаточно продвинутая наука чувствует, как магия, не так ли ?

конструктор shared_ptr<T>(Y *p) действительно, кажется, звонит shared_ptr<T>(Y *p, D d) здесь d - автоматически сгенерированный делетер для объекта.

когда это происходит, тип объекта Y известно, так что делетер для этого

Я собираюсь ответить на этот вопрос (2 года спустя), используя очень упрощенную реализацию shared_ptr, которую пользователь поймет.

сначала я перейду к нескольким боковым классам, shared_ptr_base, sp_counted_base sp_counted_impl и checked_deleter, последний из которых является шаблоном.

class sp_counted_base
{
 public:
    sp_counted_base() : refCount( 1 )
    {
    }

    virtual ~sp_deleter_base() {};
    virtual void destruct() = 0;

    void incref(); // increases reference count
    void decref(); // decreases refCount atomically and calls destruct if it hits zero

 private:
    long refCount; // in a real implementation use an atomic int
};

template< typename T > class sp_counted_impl : public sp_counted_base
{
 public:
   typedef function< void( T* ) > func_type;
    void destruct() 
    { 
       func(ptr); // or is it (*func)(ptr); ?
       delete this; // self-destructs after destroying its pointer
    }
   template< typename F >
   sp_counted_impl( T* t, F f ) :
       ptr( t ), func( f )

 private:

   T* ptr; 
   func_type func;
};

template< typename T > struct checked_deleter
{
  public:
    template< typename T > operator()( T* t )
    {
       size_t z = sizeof( T );
       delete t;
   }
};

class shared_ptr_base
{
private:
     sp_counted_base * counter;

protected:
     shared_ptr_base() : counter( 0 ) {}

     explicit shared_ptr_base( sp_counter_base * c ) : counter( c ) {}

     ~shared_ptr_base()
     {
        if( counter )
          counter->decref();
     }

     shared_ptr_base( shared_ptr_base const& other )
         : counter( other.counter )
     {
        if( counter )
            counter->addref();
     }

     shared_ptr_base& operator=( shared_ptr_base& const other )
     {
         shared_ptr_base temp( other );
         std::swap( counter, temp.counter );
     }

     // other methods such as reset
};

Теперь я собираюсь создать две "свободные" функции с именем make_sp_counted_impl, которые будут возвращать указатель на вновь созданный.

template< typename T, typename F >
sp_counted_impl<T> * make_sp_counted_impl( T* ptr, F func )
{
    try
    {
       return new sp_counted_impl( ptr, func );
    }
    catch( ... ) // in case the new above fails
    {
        func( ptr ); // we have to clean up the pointer now and rethrow
        throw;
    }
}

template< typename T > 
sp_counted_impl<T> * make_sp_counted_impl( T* ptr )
{
     return make_sp_counted_impl( ptr, checked_deleter<T>() );
}

ОК, эти две функции необходимы для того, что произойдет дальше, когда вы создадите shared_ptr через шаблонную функцию.

template< typename T >
class shared_ptr : public shared_ptr_base
{

 public:
   template < typename U >
   explicit shared_ptr( U * ptr ) :
         shared_ptr_base( make_sp_counted_impl( ptr ) )
   {
   }

  // implement the rest of shared_ptr, e.g. operator*, operator->
};

обратите внимание, что происходит выше, если T является пустым, а U-ваш "тестовый" класс. Он вызовет make_sp_counted_impl () с указателем на U, а не указателем на T. управление уничтожением осуществляется здесь. Класс shared_ptr_base управляет подсчетом ссылок в отношении копирования и назначения и т. д. Класс shared_ptr сам управляет типобезопасным использованием перегрузок оператора ( - > , * etc).

таким образом, хотя у вас есть shared_ptr для void, под ним вы управляете указателем типа, который вы передали в new. Обратите внимание, что если вы преобразуете свой указатель в void*, прежде чем поместить его в shared_ptr, он не сможет скомпилироваться на checked_delete, так что вы на самом деле в безопасности там тоже.

Test* неявно преобразуется в void*, поэтому shared_ptr<Test> неявно преобразуется в shared_ptr<void>, из памяти. Это работает, потому что shared_ptr предназначен для управления разрушением во время выполнения, а не во время компиляции, они будут внутренне использовать наследование для вызова соответствующего деструктора, как это было во время выделения.