Dlaczego std :: shared_ptr <void> działa

129

Znalazłem kod używający std :: shared_ptr do wykonania dowolnego czyszczenia przy wyłączaniu. Na początku myślałem, że ten kod nie może działać, ale potem wypróbowałem następujące:

#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;
}

Ten program daje wynik:

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

Mam kilka pomysłów, dlaczego to może zadziałać, które mają związek z wewnętrznymi elementami std :: shared_ptrs zaimplementowanymi dla G ++. Ponieważ obiekty te zawijają wewnętrzny wskaźnik razem z licznikiem, z którego rzutowanie std::shared_ptr<test>do std::shared_ptr<void>prawdopodobnie nie przeszkadza w wywołaniu destruktora. Czy to założenie jest słuszne?

I oczywiście o wiele ważniejsze pytanie: czy to gwarantuje normalne działanie, czy może dalsze zmiany w wewnętrznych elementach std :: shared_ptr, inne implementacje faktycznie psują ten kod?

LiKao
źródło
2
Czego spodziewałeś się zamiast tego?
Wyścigi lekkości na orbicie
1
Nie ma tam żadnego rzutowania - jest to konwersja z shared_ptr <test> do shared_ptr <void>.
Alan Stokes
Do Twojej wiadomości: tutaj jest link do artykułu o std :: shared_ptr w MSDN: msdn.microsoft.com/en-us/library/bb982026.aspx, a to jest dokumentacja z GCC: gcc.gnu.org/onlinedocs/libstdc++/latest -doxygen / a00267.html
yasouser

Odpowiedzi:

100

Sztuczka polega na tym, że std::shared_ptrusuwa typ. Zasadniczo, kiedy shared_ptrtworzony jest nowy , przechowuje wewnętrznie deleterfunkcję (która może być podana jako argument dla konstruktora, ale jeśli nie jest obecna, domyślnie wywołuje delete). Po shared_ptrzniszczeniu wywołuje tę przechowywaną funkcję, a to wywoła deleter.

Prosty szkic usuwania typów, który jest uproszczony za pomocą std :: function i unikając wszelkiego liczenia odwołań i innych problemów, można zobaczyć tutaj:

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)

Kiedy a shared_ptrjest kopiowany (lub domyślnie konstruowany) z innego, element usuwający jest przekazywany dookoła, więc kiedy konstruujesz a shared_ptr<T>z a, shared_ptr<U>informacje o tym, który destruktor do wywołania, są również przekazywane w pliku deleter.

David Rodríguez - dribeas
źródło
Nie wydaje się być pomyłka: my_shared. Naprawiłbym to, ale nie mam jeszcze uprawnień do edycji.
Alexey Kukanov
@Alexey Kukanov, @Dennis Zickefoose: Dzięki za edycję byłem nieobecny i go nie widziałem.
David Rodríguez - dribeas
2
@ user102008 nie potrzebujesz 'std :: function', ale jest trochę bardziej elastyczny (prawdopodobnie w ogóle nie ma to znaczenia), ale to nie zmienia sposobu działania wymazywania typu, jeśli przechowujesz 'delete_deleter <T>' jako wskaźnik funkcji 'void (void *)' wykonujesz tam wymazanie typu: T zniknął z przechowywanego typu wskaźnika.
David Rodríguez - dribeas
1
Takie zachowanie gwarantuje standard C ++, prawda? Potrzebuję wymazywania typów w jednej z moich klas i std::shared_ptr<void>pozwala mi uniknąć deklarowania bezużytecznej klasy opakowującej tylko po to, aby móc dziedziczyć ją z określonej klasy bazowej.
Violet Giraffe
1
@AngelusMortis: dokładny usuwacz nie jest częścią typu my_unique_ptr. Gdy w mainszablonie jest doubletworzona instancja, wybrany jest właściwy element usuwający, ale nie jest to część typu my_unique_ptri nie można go pobrać z obiektu. Typ elementu usuwającego jest usuwany z obiektu, gdy funkcja otrzymuje my_unique_ptr(powiedzmy przez odniesienie do wartości r), funkcja ta nie wie i nie musi wiedzieć, czym jest element usuwający.
David Rodríguez - dribeas
35

shared_ptr<T> logicznie [*] ma (co najmniej) dwóch odpowiednich członków danych:

  • wskaźnik do zarządzanego obiektu
  • wskaźnik do funkcji usuwającej, która zostanie użyta do jej zniszczenia.

Twoja funkcja usuwająca shared_ptr<Test>, biorąc pod uwagę sposób, w jaki ją zbudowałeś, jest normalną funkcją Test, która konwertuje wskaźnik na Test*i deleteto.

Kiedy wepchniesz swój shared_ptr<Test>do wektora shared_ptr<void>, oba są kopiowane, chociaż pierwszy jest konwertowany na void*.

Tak więc, gdy element wektora zostanie zniszczony, biorąc z nim ostatnie odniesienie, przekazuje wskaźnik do elementu usuwającego, który niszczy go poprawnie.

W rzeczywistości jest to trochę bardziej skomplikowane, ponieważ shared_ptrmoże przyjmować funktor usuwający, a nie tylko funkcję, więc mogą nawet być przechowywane dane na obiekt, a nie tylko wskaźnik funkcji. Ale w tym przypadku nie ma takich dodatkowych danych, wystarczyłoby po prostu zapisać wskaźnik do instancji funkcji szablonu, z parametrem szablonu, który przechwytuje typ, przez który wskaźnik ma zostać usunięty.

[*] logicznie w tym sensie, że ma do nich dostęp - mogą nie być członkami samego shared_ptr, ale zamiast jakiegoś węzła zarządzania, na który wskazuje.

Steve Jessop
źródło
3
+1 za wzmiankę, że funkcja / funktor usuwający jest kopiowana do innych instancji shared_ptr - fragment informacji pominięty w innych odpowiedziach.
Alexey Kukanov
Czy to oznacza, że ​​podczas korzystania z shared_ptrs nie są potrzebne wirtualne podstawowe destruktory?
ronag
@ronag Yes. Jednak nadal zalecałbym uczynienie destruktora wirtualnym, przynajmniej jeśli masz innych wirtualnych członków. (Ból związany z przypadkowym zapomnieniem przeważa nad możliwymi korzyściami.)
Alan Stokes
Tak, zgadzam się. Ciekawe, ale nie mniej. Wiedziałem o usuwaniu czcionek, ale nie brałem pod uwagę tej „funkcji”.
ronag
2
@ronag: wirtualne destruktory nie są wymagane, jeśli tworzysz shared_ptrbezpośrednio z odpowiednim typem lub jeśli używasz make_shared. Ale nadal jest to dobry pomysł, jako rodzaj wskaźnika można zmienić z budową dopóki jest on przechowywany w shared_ptr: base *p = new derived; shared_ptr<base> sp(p);, o ile shared_ptrdotyczy przedmiot jest basenie derived, więc trzeba wirtualnego destruktora. Ten wzór może być na przykład typowy dla wzorców fabrycznych.
David Rodríguez - dribeas
10

Działa, ponieważ używa wymazywania typów.

Zasadniczo, kiedy budujesz a shared_ptr, przekazuje jeden dodatkowy argument (który możesz podać, jeśli chcesz), którym jest funktor usuwający.

Ten domyślny funktor przyjmuje jako argument wskaźnik do typu, którego używasz w shared_ptrelemencie, więc voidtutaj rzutuje go odpowiednio na typ statyczny, którego użyłeś testtutaj, i wywołuje destruktor na tym obiekcie.

Jakakolwiek wystarczająco zaawansowana nauka przypomina magię, prawda?

Matthieu M.
źródło
5

shared_ptr<T>(Y *p)Wygląda na to, że konstruktor rzeczywiście wywołuje shared_ptr<T>(Y *p, D d)where djest automatycznie generowanym usuwaniem obiektu.

Kiedy tak się dzieje, typ obiektu Yjest znany, więc usuwacz tego shared_ptrobiektu wie, który destruktor wywołać, a informacja ta nie jest tracona, gdy wskaźnik jest przechowywany w wektorze shared_ptr<void>.

Rzeczywiście, specyfikacje wymagają, aby shared_ptr<T>obiekt odbierający zaakceptował shared_ptr<U>obiekt, musi być prawdą, że i U*musi być niejawnie konwertowany na T*a, co z pewnością ma miejsce w przypadku, T=voidgdy każdy wskaźnik może zostać przekonwertowany na void*niejawny. Nic nie jest powiedziane o usuwaczu, który będzie nieważny, więc rzeczywiście specyfikacje nakazują, aby to działało poprawnie.

Z technicznego punktu widzenia IIRC a shared_ptr<T>zawiera wskaźnik do ukrytego obiektu, który zawiera licznik odniesienia i wskaźnik do rzeczywistego obiektu; przechowując deleter w tej ukrytej strukturze można sprawić, by ta pozornie magiczna funkcja działała przy zachowaniu shared_ptr<T>wielkości zwykłego wskaźnika (jednak dereferencja wskaźnika wymaga podwójnego pośredniego

shared_ptr -> hidden_refcounted_object -> real_object
6502
źródło
3

Test*jest niejawnie konwertowany na void*, dlatego shared_ptr<Test>jest niejawnie konwertowany na shared_ptr<void>, z pamięci. Działa shared_ptrto, ponieważ jest przeznaczone do kontrolowania niszczenia w czasie wykonywania, a nie kompilacji, będą wewnętrznie używać dziedziczenia, aby wywołać odpowiedni destruktor, tak jak to było w czasie alokacji.

Szczeniak
źródło
Czy możesz to bardziej wyjaśnić? Właśnie opublikowałem podobne pytanie, byłoby wspaniale, gdybyś mógł pomóc!
Bruce
3

Odpowiem na to pytanie (2 lata później), używając bardzo uproszczonej implementacji shared_ptr, którą użytkownik zrozumie.

Najpierw przejdę do kilku klas pobocznych, shared_ptr_base, sp_counted_base sp_counted_impl i Checked_deleter, z których ostatnią jest szablon.

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

Teraz utworzę dwie „darmowe” funkcje o nazwie make_sp_counted_impl, które zwrócą wskaźnik do nowo utworzonej.

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

Ok, te dwie funkcje są niezbędne, jeśli chodzi o to, co będzie dalej, gdy utworzysz shared_ptr za pomocą funkcji szablonowej.

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

Zwróć uwagę, co dzieje się powyżej, jeśli T jest void, a U jest twoją klasą „testową”. Wywoła metodę make_sp_counted_impl () ze wskaźnikiem na U, a nie wskaźnikiem na T. Zarządzanie zniszczeniem odbywa się tutaj. Klasa shared_ptr_base zarządza zliczaniem odwołań w odniesieniu do kopiowania i przypisywania itp. Klasa shared_ptr sama zarządza bezpiecznym dla typów wykorzystaniem przeciążeń operatorów (->, * itd.).

Tak więc, chociaż masz shared_ptr do unieważnienia, pod spodem zarządzasz wskaźnikiem typu, który przekazałeś do nowego. Zauważ, że jeśli przekonwertujesz swój wskaźnik na void * przed umieszczeniem go w shared_ptr, kompilacja nie powiedzie się na check_delete, więc jesteś tam bezpieczny.

Dojną krową
źródło