Właśnie oglądałem strumienie „Going Native 2012” i zauważyłem dyskusję std::shared_ptr
. Byłem nieco zaskoczony, słysząc nieco negatywny pogląd Bjarne'a std::shared_ptr
i jego komentarz, że powinien on zostać użyty jako „ostateczność”, gdy czas życia obiektu jest niepewny (co, moim zdaniem, rzadko powinno tak być).
Czy ktoś chciałby wyjaśnić to nieco głębiej? Jak możemy programować bez std::shared_ptr
i nadal zarządzać żywotnością obiektów w bezpieczny sposób?
c++
c++11
smart-pointer
ronag
źródło
źródło
Odpowiedzi:
Jeśli możesz uniknąć współwłasności, Twoja aplikacja będzie prostsza i łatwiejsza do zrozumienia, a zatem mniej podatna na błędy wprowadzane podczas konserwacji. Złożone lub niejasne modele własności prowadzą zwykle do trudnych do naśladowania połączeń różnych części aplikacji poprzez stan współdzielony, który może nie być łatwo prześledzić.
Biorąc to pod uwagę, lepiej jest używać obiektów z automatycznym czasem przechowywania i mieć „podrzędne” wartości. W przeciwnym razie
unique_ptr
może być dobrą alternatywą dlashared_ptr
bycia - jeśli nie w ostateczności - jakimś sposobem na liście pożądanych narzędzi.źródło
Świat, w którym żyje Bjarne, jest bardzo ... akademicki, z braku lepszego określenia. Jeśli kod można zaprojektować i ustrukturyzować w taki sposób, że obiekty mają bardzo przemyślane hierarchie relacyjne, tak że relacje własności są sztywne i nieugięte, kod przepływa w jednym kierunku (od wysokiego poziomu do niskiego poziomu), a obiekty rozmawiają tylko z tymi niższymi hierarchii, wtedy nie będzie wiele potrzeby
shared_ptr
. Jest to coś, czego używasz w tych rzadkich przypadkach, gdy ktoś musi złamać zasady. Ale w przeciwnym razie możesz po prostu wsadzić wszystko wvector
s lub inne struktury danych, które używają semantyki wartości, iunique_ptr
s dla rzeczy, które musisz przydzielić pojedynczo.Chociaż jest to świetny świat do życia, to nie jest to, co możesz robić przez cały czas. Jeśli nie możesz zorganizować swojego kodu w ten sposób, ponieważ konstrukcja systemu, który próbujesz stworzyć, oznacza, że jest to niemożliwe (lub po prostu głęboko nieprzyjemne), będziesz potrzebować wspólnej własności obiektów coraz bardziej .
W takim systemie trzymanie nagich wskazówek nie jest ... dokładnie niebezpieczne, ale rodzi pytania. Wspaniałą rzeczą
shared_ptr
jest to, że zapewnia rozsądne składniowe gwarancje dotyczące żywotności obiektu. Czy można to zepsuć? Oczywiście. Ale ludzie mogą takżeconst_cast
rzeczy; podstawowa opieka i karmienieshared_ptr
powinny zapewniać rozsądną jakość życia przydzielonym obiektom, których własność musi być dzielona.Są też
weak_ptr
s, których nie można użyć przy braku ashared_ptr
. Jeśli twój system ma sztywną strukturę, możesz zapisać nagi wskaźnik do jakiegoś obiektu, wiedząc, że struktura aplikacji zapewnia, że wskazany obiekt przeżyje cię. Możesz wywołać funkcję, która zwraca wskaźnik do jakiejś wartości wewnętrznej lub zewnętrznej (na przykład znajdź obiekt o nazwie X). We właściwie skonstruowanym kodzie funkcja ta byłaby dostępna tylko wtedy, gdy gwarantowano by, że czas życia obiektu przekroczy twój; w ten sposób przechowywanie nagiego wskaźnika w obiekcie jest w porządku.Ponieważ tej sztywności nie zawsze można osiągnąć w rzeczywistych systemach, potrzebujesz sposobu, aby zapewnić rozsądną trwałość. Czasami nie potrzebujesz pełnej własności; Czasami musisz po prostu wiedzieć, kiedy wskaźnik jest zły lub dobry. Tam właśnie
weak_ptr
przychodzi. Były przypadki, w których mogłem użyćunique_ptr
lubboost::scoped_ptr
, ale musiałem użyć,shared_ptr
ponieważ szczególnie potrzebowałem dać komuś „niestabilny” wskaźnik. Wskaźnik, którego życie było nieokreślone, mogli zapytać, kiedy wskaźnik ten zostanie zniszczony.Bezpieczny sposób na przetrwanie, gdy stan świata jest nieokreślony.
Czy można to zrobić za pomocą jakiegoś wywołania funkcji, aby uzyskać wskaźnik, zamiast przez
weak_ptr
? Tak, ale to może być łatwiejsze do złamania. Funkcja, która zwraca nagi wskaźnik, nie ma możliwości syntaktycznego sugerowania, że użytkownik nie robi czegoś takiego jak przechowywanie tego wskaźnika w dłuższej perspektywie. Zwrócenie ashared_ptr
także sprawia, że zbyt łatwo jest go po prostu przechowywać i potencjalnie przedłużyć żywotność obiektu. Zwrócenieweak_ptr
jednak zdecydowanie sugeruje, że przechowywanie tego,shared_ptr
co otrzymujesz,lock
jest ... wątpliwym pomysłem. Nie powstrzyma cię to przed zrobieniem tego, ale nic w C ++ nie powstrzyma cię przed złamaniem kodu.weak_ptr
zapewnia minimalny opór przed robieniem rzeczy naturalnych.To nie znaczy, że
shared_ptr
nie można tego nadużywać ; z pewnością może. Szczególnie wcześniej-unique_ptr
było wiele przypadków, w których właśnie użyłem,boost::shared_ptr
ponieważ musiałem przekazać wskaźnik RAII lub umieścić go na liście. Bez przemieszczania się i semantykiunique_ptr
,boost::shared_ptr
był jedynym realnym rozwiązaniem.I możesz go używać w miejscach, w których jest to zupełnie niepotrzebne. Jak wspomniano powyżej, odpowiednia struktura kodu może wyeliminować potrzebę niektórych zastosowań
shared_ptr
. Ale jeśli twój system nie może być skonstruowany jako taki i nadal robi to, co musi,shared_ptr
będzie bardzo przydatny.źródło
shared_ptr
jest świetny dla systemów, w których c ++ jest zintegrowany z językiem skryptowym, takim jak Python. Korzystanieboost::python
, liczenie referencji po stronie c ++ i python współpracuje bardzo dobrze; dowolny obiekt z c ++ może być nadal przechowywany w Pythonie i nie umrze.shared_ptr
. Oba używają własnych implementacjiintrusive_ptr
. Podnoszę to tylko dlatego, że oba są przykładami dużych aplikacji napisanych w C ++ w prawdziwym świecieshared_ptr
dotyczy w równym stopniuintrusive_ptr
: sprzeciwia się całej koncepcji współwłasności, a nie konkretnej pisowni tej koncepcji. Na potrzeby tego pytania są to dwa rzeczywiste przykłady dużych aplikacji, które z nich korzystająshared_ptr
. (Co więcej, pokazują, żeshared_ptr
jest to użyteczne, nawet jeśli nie pozwalaweak_ptr
.)Nie wierzę, że kiedykolwiek użyłem
std::shared_ptr
.Przez większość czasu obiekt jest powiązany z jakąś kolekcją, do której należy przez cały okres jego istnienia. W takim przypadku możesz po prostu użyć
whatever_collection<o_type>
lubwhatever_collection<std::unique_ptr<o_type>>
, że ta kolekcja jest członkiem obiektu lub zmiennej automatycznej. Oczywiście, jeśli nie potrzebujesz dynamicznej liczby obiektów, możesz po prostu użyć automatycznej tablicy o stałym rozmiarze.Ani iteracja przez kolekcję, ani żadna inna operacja na obiekcie nie wymaga funkcji pomocniczej do współwłasności ... używa obiektu, a następnie zwraca, a obiekt wywołujący gwarantuje, że obiekt pozostanie przy życiu przez całe wywołanie . Jest to zdecydowanie najczęściej stosowana umowa między dzwoniącym a odbiorcą.
Nicol Bolas skomentował, że „Jeśli jakiś obiekt trzyma się nagiego wskaźnika i obiekt ten umiera ... ups”. oraz „Przedmioty muszą zapewnić, że obiekt żyje przez życie tego obiektu. Tylko to
shared_ptr
może zrobić”.Nie kupuję tego argumentu. Przynajmniej nie to
shared_ptr
rozwiązuje ten problem. Co powiesz na:Podobnie jak wyrzucanie elementów bezużytecznych, domyślne użycie
shared_ptr
zachęca programistę, aby nie myślał o kontrakcie między obiektami lub między funkcją a wywołującym. Konieczne jest zastanowienie się nad prawidłowymi warunkami wstępnymi i późniejszymi, a czas życia obiektu to tylko mały kawałek tego większego ciasta.Obiekty nie „giną”, jakiś fragment kodu je niszczy. A rzucanie się
shared_ptr
na problem zamiast rozpracowywania umowy o połączenie jest fałszywym bezpieczeństwem.źródło
shared_ptr
iweak_ptr
zostały zaprojektowane, aby uniknąć. Bjarne stara się żyć w świecie, w którym wszystko ma ładny, wyraźny czas życia i wszystko jest wokół tego zbudowane. A jeśli potrafisz zbudować ten świat, świetnie. Ale tak nie jest w prawdziwym świecie. Przedmioty muszą zapewnić, że obiekt żyje przez życie tego obiektu. Tylkoshared_ptr
może to zrobić.shared_ptr
ogranicza tylko jedną konkretną modyfikację zewnętrzną, a nawet najczęstszą. I to nie obiekt ponosi odpowiedzialności za zapewnienie, że jego żywotność jest poprawna, jeśli umowa wywołania funkcji określa inaczej.unique_ptr
, że istnieje tylko jeden wskaźnik do obiektu i ma on własność.shared_ptr
, powinien nadal zwrócić aunique_ptr
. Konwersja zunique_ptr
nashared_ptr
jest łatwa, ale odwrotność jest logicznie niemożliwa.Wolę nie myśleć w kategoriach bezwzględnych (jak „ostateczność”), ale w odniesieniu do dziedziny problemowej.
C ++ może oferować wiele różnych sposobów zarządzania czasem życia. Niektóre z nich próbują przywrócić obiekty w sposób sterowany stosami. Niektórzy próbują ominąć to ograniczenie. Niektóre z nich są „dosłowne”, inne są przybliżeniami.
Właściwie możesz:
Person
posiadającename
tę samą osobę to ta sama osoba (lepiej: dwa przedstawienie tej samej osoby ). Żywotność jest zapewniona przez stos maszyn, w końcu - zasadniczo - nie ma znaczenia dla programu (ponieważ osoba to imię , bez względu na to, coPerson
ją nosi)std::unique_ptr
robi (możesz myśleć o tym jak o wektorze o rozmiarze 1). Ponownie przyznajesz, że obiekt zaczyna istnieć (i kończy ich istnienie) przed (po) strukturą danych, do której się odnoszą.Słabość tych metod polega na tym, że typy obiektów i wielkości nie mogą się różnić podczas wykonywania wywołań na poziomie stosu w odniesieniu do miejsca ich utworzenia. Wszystkie te techniki „zawodzą” swoją siłę we wszystkich sytuacjach, w których tworzenie i usuwanie obiektu są konsekwencją działań użytkownika, tak że typ środowiska wykonawczego obiektu nie jest znany w czasie kompilacji i mogą istnieć nadstruktury odnoszące się do obiektów użytkownik prosi o usunięcie z głębszego wywołania funkcji na poziomie stosu. W takich przypadkach musisz:
C ++ isteslf nie ma żadnego natywnego mechanizmu do monitorowania tego zdarzenia (
while(are_they_needed)
), dlatego musisz dokonać aproksymacji za pomocą:Przechodząc do pierwszego rozwiązania ostatniego, ilość struktury danych pomocniczych wymagana do zarządzania czasem życia obiektu rośnie, ponieważ czas poświęcony na jego uporządkowanie i utrzymanie.
Kosz na śmieci ma koszt, share_ptr ma mniej, Unique_ptr jeszcze mniej, a obiektów zarządzanych na stosie jest bardzo mało.
Czy
shared_ptr
„ostateczność”? Nie, to nie jest: w ostateczności są śmieciarki.shared_ptr
jest faktyczniestd::
proponowaną ostatecznością. Ale może być właściwym rozwiązaniem, jeśli jesteś w sytuacji, którą wyjaśniłem.źródło
Jedną rzeczą, o której wspominał Herb Sutter w późniejszej sesji, jest to, że za każdym razem, gdy kopiujesz,
shared_ptr<>
musi istnieć zablokowany przyrost / spadek. W przypadku kodu wielowątkowego w systemie wielordzeniowym synchronizacja pamięci nie jest nieznaczna. Biorąc pod uwagę wybór, lepiej użyć albo wartości stosu, albo aunique_ptr<>
i przekazywać odniesienia lub surowe wskaźniki.źródło
shared_ptr
omiń wartość lvalue lub rvalue ...shared_ptr
tak, jakby to była srebrna kula, która rozwiąże wszystkie problemy z wyciekiem pamięci tylko dlatego, że jest w standardzie. To kusząca pułapka, ale nadal ważne jest, aby zdawać sobie sprawę z własności zasobów, a jeśli ta własność nie jest dzielona, ashared_ptr<>
nie jest najlepszą opcją.Nie pamiętam, czy słowo „ostatnia ucieczka” było dokładnie tym słowem, którego użył, ale wierzę, że faktyczne znaczenie tego, co powiedział, było ostatnim „wyborem”: biorąc pod uwagę jasne warunki własności; swoje miejsce mają unikalne_ptr, słaby_ptr, wspólny_ptr, a nawet nagie wskaźniki.
Wszyscy zgodzili się, że my (programiści, autorzy książek itp.) Jesteśmy w „fazie nauki” C ++ 11, a wzorce i style są definiowane.
Jako przykład Herb wyjaśnił, że powinniśmy spodziewać się nowych wydań niektórych z przełomowych książek o C ++, takich jak Effective C ++ (Meyers) i C ++ Coding Standards (Sutter i Alexandrescu), za kilka lat, podczas gdy doświadczenie branży i najlepsze praktyki z C ++ 11 patelni na zewnątrz.
źródło
Myślę, że chodzi mu o to, że powszechne staje się pisanie share_ptr za każdym razem, gdy mogliby napisać standardowy wskaźnik (jak rodzaj globalnej zamiany), i że jest on używany jako wyłudzanie zamiast faktycznego projektowania lub przynajmniej planowanie tworzenia i usuwania obiektów.
Inną rzeczą, o której ludzie zapominają (oprócz blokowania / aktualizacji / odblokowywania wąskiego gardła wspomnianego w powyższym materiale) jest to, że sam shared_ptr nie rozwiązuje problemów z cyklem. Nadal możesz przeciekać zasoby za pomocą shared_ptr:
Obiekt A zawiera wspólny wskaźnik do innego obiektu A Obiekt B tworzy A a1 i A a2 i przypisuje a1.otherA = a2; i a2.otherA = a1; Teraz wspólne wskaźniki obiektu B, którego użył do utworzenia a1, a2, wykraczają poza zakres (powiedzmy na końcu funkcji). Teraz masz wyciek - nikt inny nie odnosi się do a1 i a2, ale odnoszą się one do siebie, więc ich liczba referencyjna wynosi zawsze 1, a ty wyciekłeś.
To prosty przykład, kiedy dzieje się to w prawdziwym kodzie, dzieje się to zwykle w skomplikowany sposób. Istnieje rozwiązanie ze słabym_ptr, ale tak wielu ludzi teraz robi po prostu share_ptr wszędzie i nawet nie wie o problemie z wyciekiem lub nawet o słabym_ptr.
Podsumowując: Myślę, że komentarze, do których odwołuje się PO, sprowadzają się do tego:
Bez względu na język, w którym pracujesz (zarządzany, niezarządzany lub coś pośredniego z odniesieniami, takimi jak shared_ptr), musisz zrozumieć i celowo zdecydować o tworzeniu obiektu, czasach życia i zniszczeniu.
edycja: nawet jeśli oznacza to „nieznane, muszę użyć shared_ptr”, wciąż o tym myślałeś i robisz to celowo.
źródło
Odpowiem z mojego doświadczenia z Objective-C, językiem, w którym wszystkie obiekty są liczone i przydzielane na stercie. Ze względu na jeden sposób traktowania obiektów, programiści są o wiele łatwiejsi. Umożliwiło to zdefiniowanie standardowych reguł, które przy przestrzeganiu gwarantują niezawodność kodu i brak wycieków pamięci. Umożliwiło to również sprytne optymalizacje kompilatora, takie jak najnowszy ARC (automatyczne zliczanie referencji).
Chodzi mi o to, że shared_ptr powinno być pierwszą opcją, a nie ostatecznością. Domyślnie korzystaj z liczenia referencji i innych opcji, tylko jeśli masz pewność co do tego, co robisz. Będziesz bardziej produktywny, a Twój kod będzie bardziej niezawodny.
źródło
Spróbuję odpowiedzieć na pytanie:
C ++ ma wiele różnych sposobów wykonywania pamięci, na przykład:
struct A { MyStruct s1,s2; };
zamiast shared_ptr w zakresie klasy. Jest to przeznaczone tylko dla zaawansowanych programistów, ponieważ wymaga zrozumienia zasad działania zależności i wymaga wystarczającej kontroli zależności, aby ograniczyć je do drzewa. Ważnym aspektem jest kolejność klas w pliku nagłówkowym. Wygląda na to, że takie użycie jest już powszechne w przypadku wbudowanych typów c ++, ale jest ono używane w klasach zdefiniowanych przez programistę i wydaje się, że jest rzadziej używane z powodu tych zależności i problemów z kolejnością klas. To rozwiązanie ma również problemy z rozmiarem. Programiści widzą w tym problemy jako wymóg korzystania z deklaracji przesyłania lub niepotrzebne #include, dlatego wielu programistów powróci do gorszego rozwiązania wskaźników, a później do shared_ptr.MyClass &find_obj(int i);
+ clone () zamiastshared_ptr<MyClass> create_obj(int i);
. Wielu programistów chce tworzyć fabryki do tworzenia nowych obiektów. shared_ptr idealnie nadaje się do tego rodzaju zastosowań. Problem polega na tym, że zakłada on już złożone rozwiązanie do zarządzania pamięcią przy użyciu alokacji sterty / wolnego magazynu, zamiast prostszego rozwiązania stosowego lub obiektowego. Dobra hierarchia klas C ++ obsługuje wszystkie schematy zarządzania pamięcią, a nie tylko jeden z nich. Rozwiązanie oparte na referencjach może działać, jeśli zwracany obiekt jest przechowywany w obiekcie zawierającym, zamiast używać lokalnej zmiennej zasięgu funkcji. Należy unikać przenoszenia własności z fabryki na kod użytkownika. Kopiowanie obiektu po użyciu find_obj () jest dobrym sposobem na jego obsługę - poradzą sobie z nim normalne konstruktory kopii i normalny konstruktor (innej klasy) z parametrem odwołania lub clone () dla obiektów polimorficznych.źródło
unique_ptr
najlepiej nadaje się do fabryk. Możesz zmienić aunique_ptr
wshared_ptr
, ale logicznie niemożliwe jest pójście w innym kierunku.