Почему std :: shared_ptr <void> работает

129

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

LiKao
источник
2
Что вы ожидали вместо этого?
Гонки легкости на орбите
1
Там нет приведения - это преобразование из shared_ptr <test> в shared_ptr <void>.
Алан Стоукс,
К вашему сведению: вот ссылка на статью о std :: shared_ptr в MSDN: msdn.microsoft.com/en-us/library/bb982026.aspx, а это документация от GCC: gcc.gnu.org/onlinedocs/libstdc++/latest -doxygen / a00267.html
yasouser

Ответы:

99

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

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

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

Давид Родригес - дрибеас
источник
Там , кажется, опечатка: my_shared. Я бы исправил это, но пока не имею права редактировать.
Алексей Куканов
@Alexey Kukanov, @Dennis Zickefoose: Спасибо за редактирование. Меня не было и я не видел.
Дэвид Родригес - дрибэас
2
@ user102008 вам не нужен 'std :: function', но он немного более гибкий (вероятно, здесь вообще не имеет значения), но это не меняет принцип работы стирания типов, если вы сохраняете 'delete_deleter <T>' как указатель функции 'void (void *)', в котором вы выполняете стирание типа: T исчез из сохраненного типа указателя.
Дэвид Родригес - dribeas
1
Такое поведение гарантируется стандартом C ++, верно? Мне нужно стирание типа в одном из моих классов, и это std::shared_ptr<void>позволяет мне избегать объявления бесполезного класса-оболочки, чтобы я мог унаследовать его от определенного базового класса.
Violet Giraffe
1
@AngelusMortis: точное средство удаления не относится к типу my_unique_ptr. Когда в mainшаблоне doubleсоздается экземпляр, выбирается правильный удалитель, но он не является частью типа my_unique_ptrи не может быть получен из объекта. Тип удалителя стирается из объекта, когда функция получает my_unique_ptr(скажем, по rvalue-ссылке), эта функция не знает и не должна знать, что такое удалитель.
Дэвид Родригес - dribeas
35

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

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

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

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

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

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

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

Стив Джессоп
источник
2
+1 за упоминание о том, что функция / функтор удаления копируется в другие экземпляры shared_ptr - часть информации, пропущенная в других ответах.
Алексей Куканов
Означает ли это, что деструкторы виртуальной базы не нужны при использовании shared_ptrs?
ronag
@ronag Да. Однако я все же рекомендую сделать деструктор виртуальным, по крайней мере, если у вас есть другие виртуальные члены. (Боль от случайного забывания перевешивает любую возможную пользу.)
Алан Стоукс,
Да, согласен. Тем не менее интересно. Я знал о стирании шрифтов, просто не учел эту его "особенность".
ronag
2
@ronag: виртуальные деструкторы не требуются, если вы создаете объект shared_ptrнапрямую с соответствующим типом или используете make_shared. Но, все - таки это хорошая идея , так как тип указателя может изменяться от строительства до тех пор, пока хранится в shared_ptr: base *p = new derived; shared_ptr<base> sp(p);, насколько shared_ptrобеспокоен объект baseне derived, так что вам нужен виртуальный деструктор. Этот шаблон может быть общим, например, с заводскими шаблонами.
Дэвид Родригес - дрибес,
10

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

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

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

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

Матье М.
источник
5

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

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

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

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

shared_ptr -> hidden_refcounted_object -> real_object
6502
источник
3

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

щенок
источник
Вы можете объяснить больше? Я только что разместил аналогичный вопрос, было бы здорово, если бы вы могли помочь!
Брюс
3

Я собираюсь ответить на этот вопрос (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, так что вы и там в безопасности.

Дойная корова
источник