Почему работают 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, другие реализации действительно нарушить этот код?

664   6  

6 ответов:

Фокус в том, что std::shared_ptr выполняет стирание типа. В принципе, когда создается новый shared_ptr, он будет хранить внутри себя функцию 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)

Когда shared_ptr копируется (или строится по умолчанию) из другого, делетер передается по кругу, так что при построении 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.

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

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

Когда это происходит, тип объекта Y известен, поэтому делетер для этого объекта shared_ptr знает, какой деструктор вызывать, и эта информация не теряется, когда указатель хранится в векторе shared_ptr<void>.

Действительно, спецификации требуют, чтобы для объекта receving shared_ptr<T>, чтобы принять объект shared_ptr<U>, было верно, что и U* должны быть неявно преобразуется в T*, и это, безусловно, имеет место с T=void, потому что любой указатель может быть преобразован в void* неявно. Ничего не сказано о deleter, который будет недействительным, так что действительно спецификации предписывают, что это будет работать правильно.

Технически IIRC a shared_ptr<T> содержит указатель на скрытый объект, который содержит счетчик ссылок и указатель на фактический объект; сохраняя deleter в этой скрытой структуре, можно сделать эту внешне волшебную функцию работа при сохранении размера shared_ptr<T> как обычного указателя (однако разыменование указателя требует двойной косвенности

shared_ptr -> hidden_refcounted_object -> real_object

Я собираюсь ответить на этот вопрос (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 сам управляет безопасным для типов использованием перегрузок операторов (->, * прием).

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

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

Comments

    Ничего не найдено.