std :: shared_ptr jako ostateczność?

59

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_ptri 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_ptri nadal zarządzać żywotnością obiektów w bezpieczny sposób?

ronag
źródło
8
Nie używasz wskaźników? Mając wyraźnego właściciela obiektu, który zarządza życiem?
Bo Persson
2
co z jawnie udostępnianymi danymi? Trudno nie używać wskaźników. Także std :: shared_pointer zrobiłby brudne „zarządzanie czasem życia” w tym przypadku
Kamil Klimek
6
Czy rozważałeś mniej słuchania przedstawionych rad, a bardziej argumentów za tymi radami? Wyjaśnia całkiem dobrze rodzaj systemu, w którym takie porady mogłyby działać.
Nicol Bolas,
@NicolBolas: Słuchałem rad i argumentów, ale oczywiście nie czułem, że zrozumiałem je wystarczająco dobrze.
ronag
O której godzinie mówi „w ostateczności”? Oglądając kawałek po 36 minutach w ( channel9.msdn.com/Events/GoingNative/GoingNative-2012/... ) mówi, że jest ostrożny w korzystaniu ze wskaźników, ale ogólnie ma na myśli wskaźniki, nie tylko shared_ptr i unique_ptr, ale nawet „ zwykły wskaźnik. Sugeruje, że same obiekty (a nie wskaźniki do obiektów przydzielonych z nowym) powinny być preferowane. Czy był to kawałek, o którym myślałeś w dalszej części prezentacji?
Pharap

Odpowiedzi:

55

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_ptrmoże być dobrą alternatywą dla shared_ptrbycia - jeśli nie w ostateczności - jakimś sposobem na liście pożądanych narzędzi.

CB Bailey
źródło
5
+1 za stwierdzenie, że problemem nie jest samo techno (współwłasność), ale trudności, jakie stwarza dla nas zwykłych ludzi, którzy następnie muszą rozszyfrować, co się dzieje.
Matthieu M.
Jednak takie podejście poważnie ograniczy zdolność programisty do stosowania wzorców programowania współbieżności w większości nietrywialnych klas OOP (z powodu braku możliwości kopiowania.) Ten problem został podniesiony w „Going Native 2013”.
rwong
48

Ś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 w vectors lub inne struktury danych, które używają semantyki wartości, i unique_ptrs 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_ptrjest to, że zapewnia rozsądne składniowe gwarancje dotyczące żywotności obiektu. Czy można to zepsuć? Oczywiście. Ale ludzie mogą także const_castrzeczy; podstawowa opieka i karmienie shared_ptrpowinny zapewniać rozsądną jakość życia przydzielonym obiektom, których własność musi być dzielona.

Są też weak_ptrs, których nie można użyć przy braku a shared_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_ptrprzychodzi. Były przypadki, w których mogłem użyć unique_ptrlub boost::scoped_ptr, ale musiałem użyć, shared_ptrponieważ 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 a shared_ptrtakże sprawia, że ​​zbyt łatwo jest go po prostu przechowywać i potencjalnie przedłużyć żywotność obiektu. Zwrócenie weak_ptrjednak zdecydowanie sugeruje, że przechowywanie tego, shared_ptrco otrzymujesz, lockjest ... wątpliwym pomysłem. Nie powstrzyma cię to przed zrobieniem tego, ale nic w C ++ nie powstrzyma cię przed złamaniem kodu. weak_ptrzapewnia minimalny opór przed robieniem rzeczy naturalnych.

To nie znaczy, że shared_ptrnie można tego nadużywać ; z pewnością może. Szczególnie wcześniej- unique_ptrbyło wiele przypadków, w których właśnie użyłem, boost::shared_ptrponieważ musiałem przekazać wskaźnik RAII lub umieścić go na liście. Bez przemieszczania się i semantyki unique_ptr, boost::shared_ptrbył 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_ptrbędzie bardzo przydatny.

Nicol Bolas
źródło
4
+1: Spójrz np. Na boost :: asio. Myślę, że pomysł rozciąga się na wiele obszarów, możesz nie wiedzieć w czasie kompilacji, który widget interfejsu użytkownika lub wywołanie asynchroniczne jest ostatnim, który zrzeka się obiektu, a wraz z shared_ptr nie musisz wiedzieć. Oczywiście nie ma zastosowania do każdej sytuacji, tylko kolejne (bardzo przydatne) narzędzie w przyborniku.
Guy Sirton,
3
Trochę późny komentarz; shared_ptrjest świetny dla systemów, w których c ++ jest zintegrowany z językiem skryptowym, takim jak Python. Korzystanie boost::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.
eudoxos
1
Dla porównania, moim zrozumieniem nie jest użycie WebKit ani Chromium shared_ptr. Oba używają własnych implementacji intrusive_ptr. Podnoszę to tylko dlatego, że oba są przykładami dużych aplikacji napisanych w C ++ w prawdziwym świecie
gman
1
@gman: Uważam twój komentarz za bardzo mylący, ponieważ sprzeciw Stroustrupa shared_ptrdotyczy w równym stopniu intrusive_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ą, że shared_ptrjest to użyteczne, nawet jeśli nie pozwala weak_ptr.)
ruakh
1
FWIW, aby odeprzeć twierdzenie, że Bjarne żyje w świecie akademickim: przez całą moją karierę czysto przemysłową (obejmującą współtworzenie giełdy G20 i wyłącznie MOG dla 500 000 graczy) widziałem tylko 3 przypadki, w których naprawdę potrzebowaliśmy wspólna własność. Mam 200% z Bjarne tutaj.
No-Bugs Hare
37

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>lub whatever_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_ptrmoże zrobić”.

Nie kupuję tego argumentu. Przynajmniej nie to shared_ptrrozwiązuje ten problem. Co powiesz na:

  • Jeśli jakaś tabela skrótów trzyma obiekt, a kod skrótu tego obiektu zmienia się ... ups.
  • Jeśli jakaś funkcja iteruje wektor, a element jest wstawiany do tego wektora ... ups.

Podobnie jak wyrzucanie elementów bezużytecznych, domyślne użycie shared_ptrzachę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_ptrna problem zamiast rozpracowywania umowy o połączenie jest fałszywym bezpieczeństwem.

Ben Voigt
źródło
17
@ronag: Podejrzewam, że zacząłeś go używać tam, gdzie surowy wskaźnik byłby lepszy, ponieważ „surowe wskaźniki są złe”. Ale surowe wskazówki nie są złe . Tylko uczynienie pierwszego, będącego właścicielem wskaźnika surowego wskaźnika, jest złe, ponieważ wtedy musisz ręcznie zarządzać pamięcią, co nie jest trywialne w przypadku wyjątków. Ale używanie surowych wskaźników jako uchwytów lub iteratorów jest w porządku.
Ben Voigt
4
@BenVoigt: Oczywiście trudność w omijaniu nagich wskaźników polega na tym, że nie znasz życia przedmiotów. Jeśli jakiś obiekt trzyma się nagiego wskaźnika i obiekt ten umiera ... ups. To właśnie tego rodzaju rzeczy, shared_ptri weak_ptrzostał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. Tylko shared_ptrmoże to zrobić.
Nicol Bolas,
5
@NicolBolas: To fałszywe bezpieczeństwo. Jeśli osoba wywołująca funkcję nie zapewnia zwykłej gwarancji: „Ten obiekt nie zostanie dotknięty przez żadną osobę z zewnątrz podczas wywołania funkcji”, wówczas obie strony muszą uzgodnić, jaki rodzaj modyfikacji zewnętrznych jest dozwolony. shared_ptrogranicza 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.
Ben Voigt
6
@NicolBolas: Jeśli funkcja tworzy obiekt i zwraca go wskaźnikiem, powinna być oznaczająca unique_ptr, że istnieje tylko jeden wskaźnik do obiektu i ma on własność.
Ben Voigt
6
@Nicol: Jeśli szuka wskaźnika w jakiejś kolekcji, prawdopodobnie powinien użyć dowolnego typu wskaźnika w tej kolekcji lub surowego wskaźnika, jeśli kolekcja zawiera wartości. Jeśli tworzy obiekt, a wywołujący chce shared_ptr, powinien nadal zwrócić a unique_ptr. Konwersja z unique_ptrna shared_ptrjest łatwa, ale odwrotność jest logicznie niemożliwa.
Ben Voigt
16

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:

  1. używaj semantyki czystej wartości . Działa w przypadku stosunkowo małych obiektów, w których ważne są „wartości”, a nie „tożsamości”, w których można założyć, że dwa Personposiadające nametę 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, co Personją nosi)
  2. użyj stosu przydzielonych obiektów oraz powiązanych odniesień lub wskaźników: pozwala na polimorfizm i zapewnia żywotność obiektu. Nie ma potrzeby stosowania „inteligentnych wskaźników”, ponieważ zapewnia się, że żaden obiekt nie może zostać „wskazany” przez struktury, które pozostawiają na stosie dłużej niż obiekt, na który wskazują (najpierw należy utworzyć obiekt, a następnie struktury, które się do niego odnoszą).
  3. używaj zarządzanych przez stos obiektów przydzielonych do stosu : tak robią std :: vector i wszystkie kontenery, a wat std::unique_ptrrobi (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:

  • wprowadzić dyscyplinę w zarządzaniu obiektami i powiązanymi strukturami odsyłającymi lub ...
  • przejść jakoś na ciemną stronę „ucieczki przed życiem opartym na czystym stosie”: obiekt musi wyjść niezależnie od funkcji, które je stworzyły. I musi odejść ... dopóki nie będą potrzebne .

C ++ isteslf nie ma żadnego natywnego mechanizmu do monitorowania tego zdarzenia ( while(are_they_needed)), dlatego musisz dokonać aproksymacji za pomocą:

  1. użyj współwłasności : życie obiektów jest powiązane z „licznikiem referencyjnym”: działa, jeśli „własność” może być zorganizowana hierarchicznie, kończy się niepowodzeniem tam, gdzie mogą istnieć pętle własności. To właśnie robi std :: shared_ptr. I słaby_ptr może być użyty do przerwania pętli. Działa to przez większość czasu, ale zawodzi przy dużym projektowaniu, w którym wielu projektantów pracuje w różnych zespołach i nie ma wyraźnego powodu (coś pochodzącego z pewnego wymogu) o tym, kto musi mieć coś (typowym przykładem są łańcuchy podwójnie lubiane: poprzedni ze względu na następny odnoszący się do poprzedniego lub następny będący właścicielem poprzedniego odnoszący się do następnego? W przypadku braku wymogu te rozwiązania są równoważne, aw dużym projekcie ryzykujesz je pomieszać)
  2. Użyj sterty śmieci : po prostu nie obchodzi cię całe życie. Od czasu do czasu prowadzisz kolekcjonera, a to, co jest nieosiągalne, jest uważane za „już nie potrzebne” i… cóż… hmm… zniszczone? sfinalizowane? mrożony?. Istnieje wiele modułów zbierających GC, ale nigdy nie znalazłem takiego, który byłby naprawdę świadomy C ++. Większość z nich ma wolną pamięć, nie dbając o zniszczenie obiektu.
  3. Korzystaj ze śmietnika C ++ ze świadomością C ++ , z odpowiednim standardowym interfejsem metod. Powodzenia w znalezieniu go.

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_ptrjest faktycznie std::proponowaną ostatecznością. Ale może być właściwym rozwiązaniem, jeśli jesteś w sytuacji, którą wyjaśniłem.

Emilio Garavaglia
źródło
9

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 a unique_ptr<>i przekazywać odniesienia lub surowe wskaźniki.

Zaćmienie
źródło
1
Lub shared_ptromiń wartość lvalue lub rvalue ...
ronag
8
Chodzi o to, nie używaj shared_ptrtak, 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, ​​a shared_ptr<>nie jest najlepszą opcją.
Zaćmienie
Dla mnie jest to najmniej ważny szczegół. Zobacz przedwczesną optymalizację. W większości przypadków nie powinno to wpływać na decyzję.
Guy Sirton,
1
@gbjbaanb: tak, są na poziomie procesora, ale w systemie wielordzeniowym unieważniasz pamięć podręczną i wymuszasz bariery pamięci.
Zaćmienie
4
W projekcie gry, nad którym pracowałem, stwierdziliśmy, że różnica w wydajności była bardzo znacząca, do tego stopnia, że ​​potrzebowaliśmy 2 różnych typów wskaźnika zliczonego do zera, jednego, który był bezpieczny dla wątków, jednego, który nie był.
Kylotan
7

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.

Eddie Velasquez
źródło
5

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.

zaraz
źródło
3

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.

Dimitris
źródło
1

Spróbuję odpowiedzieć na pytanie:

Jak możemy programować bez std :: shared_ptr i nadal bezpiecznie zarządzać czasem życia obiektów?

C ++ ma wiele różnych sposobów wykonywania pamięci, na przykład:

  1. Użyj 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.
  2. Użyj MyClass &find_obj(int i);+ clone () zamiast shared_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.
  3. Użycie referencji zamiast wskaźników lub shared_ptrs. Każda klasa c ++ ma konstruktory, a każdy element danych referencyjnych musi zostać zainicjowany. Takie użycie pozwala uniknąć wielu zastosowań wskaźników i shared_ptrs. Musisz tylko wybrać, czy pamięć znajduje się w obiekcie, czy poza nim, i wybierz rozwiązanie strukturalne lub referencyjne na podstawie decyzji. Problemy z tym rozwiązaniem są zwykle związane z unikaniem parametrów konstruktora, co jest powszechną, ale problematyczną praktyką i nieporozumieniem, w jaki sposób należy projektować interfejsy dla klas.
tp1
źródło
„Należy unikać przenoszenia własności z fabryki na kod użytkownika”. A co się stanie, gdy nie będzie to możliwe? „Użycie referencji zamiast wskaźników lub shared_ptrs.” Yyy ... nie. Wskaźniki można ponownie ustawić. Referencje nie mogą. Wymusza to ograniczenia czasowe budowy tego, co jest przechowywane w klasie. To nie jest praktyczne w wielu sprawach. Twoje rozwiązanie wydaje się być bardzo sztywne i nieelastyczne na potrzeby bardziej płynnego interfejsu i schematu użytkowania.
Nicol Bolas,
@Nicol Bolas: Gdy będziesz postępować zgodnie z powyższymi regułami, referencje będą używane do zależności między obiektami, a nie do przechowywania danych, jak sugerowałeś. Zależności są bardziej stabilne niż dane, więc nigdy nie wchodzimy w rozważany problem.
tp1
Oto bardzo prosty przykład. Masz encję gry, która jest przedmiotem. Musi odnosić się do innego obiektu, który jest docelową jednostką, z którą musi rozmawiać. Cele mogą się jednak zmienić. Cele mogą umrzeć w różnych punktach. I jednostka musi być w stanie poradzić sobie z tymi okolicznościami. Twoje sztywne podejście bez wskaźników nie jest w stanie poradzić sobie nawet z czymś tak prostym, jak zmiana celu, nie mówiąc już o śmierci celu.
Nicol Bolas,
@nicol bolas: oh, to jest traktowane inaczej; interfejs klasy obsługuje więcej niż jedną „jednostkę”. Zamiast mapowania 1: 1 między obiektami i bytami, użyjesz macierzy encji. Wtedy istoty giną bardzo łatwo, po prostu usuwając je z tablicy. W całej grze jest tylko niewielka liczba tablic encji, a zależności między tablicami nie zmieniają się bardzo często :)
tp1
2
Nie, unique_ptrnajlepiej nadaje się do fabryk. Możesz zmienić a unique_ptrw shared_ptr, ale logicznie niemożliwe jest pójście w innym kierunku.
Ben Voigt