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?
źródło
Odpowiedzi:
Sztuczka polega na tym, że
std::shared_ptr
usuwa typ. Zasadniczo, kiedyshared_ptr
tworzony jest nowy , przechowuje wewnętrzniedeleter
funkcję (która może być podana jako argument dla konstruktora, ale jeśli nie jest obecna, domyślnie wywołujedelete
). Poshared_ptr
zniszczeniu wywołuje tę przechowywaną funkcję, a to wywoładeleter
.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_ptr
jest kopiowany (lub domyślnie konstruowany) z innego, element usuwający jest przekazywany dookoła, więc kiedy konstruujesz ashared_ptr<T>
z a,shared_ptr<U>
informacje o tym, który destruktor do wywołania, są również przekazywane w plikudeleter
.źródło
my_shared
. Naprawiłbym to, ale nie mam jeszcze uprawnień do edycji.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.my_unique_ptr
. Gdy wmain
szablonie jestdouble
tworzona instancja, wybrany jest właściwy element usuwający, ale nie jest to część typumy_unique_ptr
i nie można go pobrać z obiektu. Typ elementu usuwającego jest usuwany z obiektu, gdy funkcja otrzymujemy_unique_ptr
(powiedzmy przez odniesienie do wartości r), funkcja ta nie wie i nie musi wiedzieć, czym jest element usuwający.shared_ptr<T>
logicznie [*] ma (co najmniej) dwóch odpowiednich członków danych: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 naTest*
idelete
to.Kiedy wepchniesz swój
shared_ptr<Test>
do wektorashared_ptr<void>
, oba są kopiowane, chociaż pierwszy jest konwertowany navoid*
.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_ptr
moż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.
źródło
shared_ptr
bezpośrednio z odpowiednim typem lub jeśli używaszmake_shared
. Ale nadal jest to dobry pomysł, jako rodzaj wskaźnika można zmienić z budową dopóki jest on przechowywany wshared_ptr
:base *p = new derived; shared_ptr<base> sp(p);
, o ileshared_ptr
dotyczy przedmiot jestbase
niederived
, więc trzeba wirtualnego destruktora. Ten wzór może być na przykład typowy dla wzorców fabrycznych.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_ptr
elemencie, więcvoid
tutaj rzutuje go odpowiednio na typ statyczny, którego użyłeśtest
tutaj, i wywołuje destruktor na tym obiekcie.Jakakolwiek wystarczająco zaawansowana nauka przypomina magię, prawda?
źródło
shared_ptr<T>(Y *p)
Wygląda na to, że konstruktor rzeczywiście wywołujeshared_ptr<T>(Y *p, D d)
whered
jest automatycznie generowanym usuwaniem obiektu.Kiedy tak się dzieje, typ obiektu
Y
jest znany, więc usuwacz tegoshared_ptr
obiektu wie, który destruktor wywołać, a informacja ta nie jest tracona, gdy wskaźnik jest przechowywany w wektorzeshared_ptr<void>
.Rzeczywiście, specyfikacje wymagają, aby
shared_ptr<T>
obiekt odbierający zaakceptowałshared_ptr<U>
obiekt, musi być prawdą, że iU*
musi być niejawnie konwertowany naT*
a, co z pewnością ma miejsce w przypadku,T=void
gdy każdy wskaźnik może zostać przekonwertowany navoid*
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 zachowaniushared_ptr<T>
wielkości zwykłego wskaźnika (jednak dereferencja wskaźnika wymaga podwójnego pośredniegoshared_ptr -> hidden_refcounted_object -> real_object
źródło
Test*
jest niejawnie konwertowany navoid*
, dlategoshared_ptr<Test>
jest niejawnie konwertowany nashared_ptr<void>
, z pamięci. Działashared_ptr
to, 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.źródło
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.
źródło