Чому робота 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, інші реалізації насправді порушують цей код?


2
Що ви очікували натомість?
Гонки легкості по орбіті

1
Там немає жодної ролі - це перетворення з shared_ptr <test> в shared_ptr <void>.
Алан Стоукс

FYI: ось посилання на статтю про std :: shared_ptr в MSDN: msdn.microsoft.com/en-us/library/bb982026.aspx, і це документація від GCC: gcc.gnu.org/onlinedocs/libstdc++/latest -doxygen / a00267.html
yasouser

Відповіді:


98

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


Там , здається, помилка: my_shared. Я б це виправив, але ще не маю привілею редагувати.
Олексій Куканов

@Alexey Kukanov, @Dennis Zickefoose: Дякую за редагування, я не був і не бачив його.
Девід Родрігес - дрибес

2
@ user102008 вам не потрібен 'std :: function', але він дещо гнучкіший (напевно, це зовсім не має значення), але це не змінює способу стирання типу, якщо ви зберігаєте 'delete_deleter <T>' як функціональний вказівник 'void (void *)', який ви виконуєте стирання типу: T відходить від збереженого типу вказівника.
Девід Родрігес - дрибес

1
Така поведінка гарантується стандартом C ++, правда? Мені потрібно стирання типу в одному з моїх класів, і std::shared_ptr<void>дозволяє мені уникати оголошення марного класу обгортки лише для того, щоб я міг успадкувати його від певного базового класу.
Фіолетова жирафа

1
@AngelusMortis: Точний делетер не є частиною типу my_unique_ptr. Коли в mainшаблоні doubleвибрано правий делетер, але він не входить до типу my_unique_ptrі не може бути вилучений з об'єкта. Тип делетера стирається з об'єкта, коли функція отримує my_unique_ptr(скажімо, за допомогою rvalue-reference), ця функція не має і не повинна знати, що таке делетер.
Девід Родрігес - дрибес

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*неявно. Про делетер, який буде недійсним, нічого не сказано, тому специфікації вимагають, щоб це працювало правильно.

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

shared_ptr -> hidden_refcounted_object -> real_object

3

Test*неявно перетворюється в void*, тому shared_ptr<Test>неявно перетворюється в shared_ptr<void>пам'ять. Це працює тому shared_ptr, що призначений для управління руйнуванням під час виконання, а не під час компіляції, вони внутрішньо використовуватимуть спадщину для виклику відповідного деструктора, як це було під час розподілу.


Чи можете ви пояснити більше? Я опублікував подібне запитання просто зараз, було б чудово, якби ви могли допомогти!
Брюс

3

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

По-перше, я переходжу до декількох бічних класів, shared_ptr_base, sp_count_base sp_count_impl і перевірив_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_count_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_count_impl () з вказівником на U, а не вказівником на T. Управління знищенням все зроблено тут. Клас shared_ptr_base керує підрахунком посилань щодо копіювання та призначення тощо. Клас shared_ptr сам керує типовим використанням перевантажень операторів (->, * тощо).

Таким чином, хоча у вас є спільний_птр недійсним, під ним ви керуєте вказівником того типу, який ви перейшли в новий. Зауважте, що якщо ви перетворите ваш покажчик на пустоту * перед тим, як помістити його в shared_ptr, він не вдасться скомпілювати на check_delete, щоб ви насправді були і там безпечні.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.