magia shared_ptr :)

89

Pan Lidström i ja pokłóciliśmy się :)

Pan Lidström twierdzi, że konstrukcja shared_ptr<Base> p(new Derived);nie wymaga, aby Base miał wirtualnego destruktora:

Armen Tsirunyan : "Naprawdę? Czy shared_ptr wyczyści się poprawnie? Czy mógłbyś w tym przypadku zademonstrować, jak można zaimplementować ten efekt?"

Daniel Lidström : „ Shared_ptr używa własnego destruktora do usunięcia instancji Concrete. W społeczności C ++ jest to znane jako RAII. Moja rada jest taka, że ​​nauczysz się wszystkiego o RAII. Dzięki temu kodowanie w C ++ będzie o wiele łatwiejsze, gdy będziesz używać RAII we wszystkich sytuacjach. "

Armen Tsirunyan : "Wiem o RAII i wiem również, że ostatecznie destruktor shared_ptr może usunąć zapisane piksele, gdy pn osiągnie 0. Ale jeśli px miałby statyczny wskaźnik typu do Basei dynamiczny wskaźnik typu do Derived, to jeśli nie Basema wirtualnego destruktora, to spowoduje niezdefiniowane zachowanie. Popraw mnie, jeśli się mylę ”.

Daniel Lidström : " Shared_ptr wie, że typ statyczny to Beton. Wie o tym, odkąd przekazałem go w jego konstruktorze! Wygląda trochę jak magia, ale mogę zapewnić, że jest to zgodne z projektem i niezwykle ładne."

Więc osądź nas. Jak jest możliwe (jeśli jest) zaimplementowanie shared_ptr bez wymagania, aby klasy polimorficzne miały wirtualny destruktor? Z góry dziękuję

Armen Tsirunyan
źródło
3
Mogłeś utworzyć link do oryginalnego wątku .
Darin Dimitrov
8
Inną interesującą rzeczą jest to, że shared_ptr<void> p(new Derived)zniszczy również Derivedobiekt przez jego destruktor, niezależnie od tego, czy jest, virtualczy nie.
dalle
7
Świetny
5
Mimo że shared_ptr na to pozwala, to naprawdę złym pomysłem jest zaprojektowanie klasy jako bazy bez wirtualnego dtora. Komentarze Daniela na temat RAII są mylące - nie ma z tym nic wspólnego - ale cytowana rozmowa brzmi jak zwykła nieporozumienie (i błędne założenie, jak działa shared_ptr).
6
Nie RAII, ale raczej typ - usuwa destruktor. Musisz być ostrożny, bo shared_ptr<T>( (T*)new U() )gdzie struct U:Tnie zrobisz tego, co właściwe (a można to zrobić pośrednio łatwo, na przykład funkcja, która przyjmuje a T*i jest przekazywana a U*)
Yakk - Adam Nevraumont

Odpowiedzi:

74

Tak, w ten sposób można zaimplementować shared_ptr. Boost robi, a standard C ++ 11 również wymaga tego zachowania. Jako dodatkowa elastyczność, shared_ptr zarządza czymś więcej niż tylko licznikiem referencyjnym. Tak zwany deleter jest zwykle umieszczany w tym samym bloku pamięci, który zawiera również liczniki odniesienia. Ale najfajniejsze jest to, że typ tego narzędzia usuwającego nie jest częścią typu shared_ptr. Nazywa się to "wymazywaniem typu" i jest zasadniczo tą samą techniką, która jest używana do implementacji "funkcji polimorficznych" boost :: function lub std :: function do ukrycia aktualnego typu funktora. Aby Twój przykład zadziałał, potrzebujemy konstruktora opartego na szablonach:

template<class T>
class shared_ptr
{
public:
   ...
   template<class Y>
   explicit shared_ptr(Y* p);
   ...
};

Więc jeśli użyjesz tego z klasami Base i Derived ...

class Base {};
class Derived : public Base {};

int main() {
   shared_ptr<Base> sp (new Derived);
}

... konstruktor oparty na szablonie z Y = Derived jest używany do konstruowania obiektu shared_ptr. Konstruktor ma zatem szansę na utworzenie odpowiedniego obiektu deleter i liczników odwołań oraz przechowuje wskaźnik do tego bloku kontrolnego jako element członkowski danych. Jeśli licznik odwołań osiągnie zero, do usunięcia obiektu zostanie użyty wcześniej utworzony i rozpoznający pochodne element usuwający.

Standard C ++ 11 ma do powiedzenia na temat tego konstruktora (20.7.2.2.1):

Wymaga: p musi być zmienna na T*. Yjest kompletnym typem. Wyrażenie delete ppowinno być dobrze sformułowane, powinno mieć dobrze zdefiniowane zachowanie i nie może powodować wyjątków.

Efekty: konstruuje shared_ptrobiekt będący właścicielem wskaźnika p.

A dla destruktora (20.7.2.2.2):

Efekty: jeśli *thisjest pusty lub ma współwłasność z inną shared_ptrinstancją ( use_count() > 1), nie ma żadnych skutków ubocznych. W przeciwnym razie, jeśli *thisjest właścicielem obiektu pi usuwania d, d(p)jest wywoływana. W przeciwnym razie, jeśli *thisjest właścicielem wskaźnika pi delete pjest wywoływana.

(podkreślenie pogrubioną czcionką jest moje).

sellibitze
źródło
the upcoming standard also requires this behaviour: (a) Którą normę i (b) czy możecie podać odniesienie (do normy)?
kevinarpe
Chcę tylko dodać komentarz do odpowiedzi @sellibitze, ponieważ nie mam wystarczającej liczby punktów add a comment. IMO, to więcej Boost does thisniż the Standard requires. Nie sądzę, by standard wymagał tego z tego, co rozumiem. Mówiąc o przykład @sellibitze „s shared_ptr<Base> sp (new Derived);, Wymaga od constructorpo prostu poprosić o delete Derivedbycie dobrze określone i dobrze wykształcone. W specyfikacji destructoristnieje również p, ale nie sądzę, że odnosi się do pw specyfikacji constructor.
Lujun Weng
28

Po utworzeniu shared_ptr przechowuje w sobie obiekt deleter . Ten obiekt jest wywoływany, gdy shared_ptr ma zwolnić wskazany zasób. Ponieważ wiesz, jak zniszczyć zasób w momencie budowy, możesz użyć shared_ptr z niekompletnymi typami. Ktokolwiek utworzył shared_ptr, zapisał tam poprawny plik usuwający.

Na przykład możesz utworzyć niestandardowy plik do usuwania:

void DeleteDerived(Derived* d) { delete d; } // EDIT: no conversion needed.

shared_ptr<Base> p(new Derived, DeleteDerived);

p wywoła DeleteDerived, aby zniszczyć wskazany obiekt. Implementacja robi to automatycznie.

Yakov Galka
źródło
4
+1 za uwagę o niekompletnych typach, bardzo przydatne, gdy używasz shared_ptrjako atrybutu.
Matthieu M.,
16

Po prostu,

shared_ptr używa specjalnej funkcji deleter, która jest tworzona przez konstruktora, który zawsze używa destruktora danego obiektu, a nie destruktora z Base, to trochę pracy z metaprogramowaniem szablonu, ale działa.

Coś w tym stylu

template<typename SomeType>
shared_ptr(SomeType *p)
{
   this->destroyer = destroyer_function<SomeType>(p);
   ...
}
Artem
źródło
1
hmm ... ciekawe, zaczynam w to wierzyć :)
Armen Tsirunyan
1
@Armen Tsirunyan Powinieneś był zajrzeć do opisu projektu shared_ptr przed rozpoczęciem dyskusji. To „przechwycenie deleter” jest jedną z podstawowych cech shared_ptr ...
Paul Michalik
6
@ paul_71: Zgadzam się z tobą. Z drugiej strony uważam, że ta dyskusja była przydatna nie tylko dla mnie, ale także dla innych osób, które nie znały tego faktu na temat shared_ptr. Więc myślę, że i tak nie było wielkim grzechem rozpoczęcie tego wątku :)
Armen Tsirunyan
3
@Armen Oczywiście, że nie. Raczej wykonałeś dobrą robotę, wskazując na tę naprawdę bardzo ważną cechę shared_ptr <T>, która jest często nadzorowana nawet przez doświadczonych programistów c ++.
Paul Michalik