C ++: Inteligentne wskaźniki, surowe wskaźniki, brak wskaźników? [Zamknięte]

48

Jakie są twoje preferowane wzorce w zakresie tworzenia gier w C ++ w odniesieniu do używania wskaźników (czy to żadnych, surowych, z zakresem, udostępnianych, czy w inny sposób pomiędzy inteligentnymi i głupimi)?

Możesz rozważyć

  • własność obiektu
  • łatwość użycia
  • kopiuj zasady
  • nad głową
  • cykliczne odniesienia
  • platforma docelowa
  • używać z pojemnikami
jmp97
źródło

Odpowiedzi:

32

Po wypróbowaniu różnych podejść, dzisiaj jestem w zgodzie z Przewodnikiem po stylu Google C ++ :

Jeśli faktycznie potrzebujesz semantyki wskaźnika, scoped_ptr jest świetny. Powinieneś używać std :: tr1 :: shared_ptr tylko w ściśle określonych warunkach, na przykład gdy obiekty muszą być trzymane przez kontenery STL. Nigdy nie powinieneś używać auto_ptr. [...]

Ogólnie rzecz biorąc, wolimy, abyśmy projektowali kod z wyraźnym prawem własności do obiektu. Najwyraźniejsze prawo własności do obiektu uzyskuje się poprzez użycie obiektu bezpośrednio jako zmiennej pola lub zmiennej lokalnej, bez użycia wskaźników. [..]

Chociaż nie są one zalecane, wskaźniki liczone w referencjach są czasem najprostszym i najbardziej eleganckim sposobem rozwiązania problemu.

jmp97
źródło
14
Dzisiaj możesz użyć std :: unique_ptr zamiast scoped_ptr.
Klaim,
24

Podążam także tokiem myślenia „silnej własności”. Lubię jasno określać, że „ta klasa jest właścicielem tego członka”, gdy jest to właściwe.

Rzadko używam shared_ptr. Jeśli to zrobię, wykorzystam to, weak_ptrkiedy tylko mogę, więc mogę traktować obiekt jak uchwyt obiektu zamiast zwiększać liczbę referencji.

Używam scoped_ptrwszędzie. Pokazuje oczywistą własność. Jedynym powodem, dla którego nie tworzę takich obiektów, jest to, że możesz je dalej zadeklarować, jeśli znajdują się w scoped_ptr.

Jeśli potrzebuję listy obiektów, używam ptr_vector. Jest bardziej wydajny i ma mniej skutków ubocznych niż używanie vector<shared_ptr>. Myślę, że możesz nie być w stanie przekazać deklaracji typu w ptr_vector (minęło trochę czasu), ale jego semantyka sprawia, że ​​warto. Zasadniczo, jeśli usuniesz obiekt z listy, zostanie on automatycznie usunięty. To także pokazuje oczywistą własność.

Jeśli potrzebuję odniesienia do czegoś, staram się, aby było to odniesienie zamiast nagiego wskaźnika. Czasami nie jest to praktyczne (tj. Kiedy potrzebujesz referencji po zbudowaniu obiektu). Tak czy inaczej, referencje pokazują oczywiście, że nie jesteś właścicielem obiektu, a jeśli podążasz za wspólną semantyką wskaźnika wszędzie indziej, to nagie wskaźniki zwykle nie powodują żadnych dodatkowych zamieszania (szczególnie jeśli przestrzegasz reguły „bez ręcznego usuwania”) .

Dzięki tej metodzie jedna gra na iPhone'a, nad którą pracowałem, mogła mieć tylko jedno deletepołączenie, i to było w mostku Obj-C do C ++, który napisałem.

Ogólnie jestem zdania, że ​​zarządzanie pamięcią jest zbyt ważne, aby pozostawić je ludziom. Jeśli możesz zautomatyzować usuwanie, powinieneś. Jeśli narzut związany z shared_ptr jest zbyt drogi w czasie wykonywania (zakładając, że wyłączyłeś obsługę wątków itp.), Prawdopodobnie powinieneś użyć czegoś innego (np. Wzorca kubełkowego), aby zmniejszyć swoje alokacje dynamiczne.

Tetrad
źródło
1
Doskonałe podsumowanie. Czy faktycznie masz na myśli shared_ptr, a nie wspomnienie o smart_ptr?
jmp97
Tak, miałem na myśli shared_ptr. Naprawię to.
Tetrad
10

Użyj odpowiedniego narzędzia do pracy.

Jeśli Twój program może zgłaszać wyjątki, upewnij się, że kod rozpoznaje wyjątki. Używanie inteligentnych wskaźników, RAII i unikanie dwufazowej budowy to dobre punkty wyjścia.

Jeśli masz cykliczne odwołania bez wyraźnej semantyki własności, możesz rozważyć użycie biblioteki czyszczenia pamięci lub refaktoryzację projektu.

Dobre biblioteki pozwolą ci zakodować koncepcję, a nie typ, więc w większości przypadków nie powinno mieć znaczenia, jakiego rodzaju wskaźnika używasz poza kwestiami zarządzania zasobami.

Jeśli pracujesz w środowisku wielowątkowym, upewnij się, że rozumiesz, czy Twój obiekt jest potencjalnie współużytkowany przez wątki. Jednym z głównych powodów, dla których warto rozważyć użycie boost :: shared_ptr lub std :: tr1 :: shared_ptr jest to, że używa licznika referencji bezpiecznego dla wątków.

Jeśli martwisz się o oddzielny przydział liczb referencyjnych, istnieje wiele sposobów na obejście tego. Korzystając z biblioteki boost :: shared_ptr, możesz w puli przydzielić liczniki referencyjne lub użyć boost :: make_shared (moje preferencje), który przydziela obiekt i liczbę referencji w pojedynczej alokacji, łagodząc w ten sposób większość obaw związanych z brakiem pamięci podręcznej. Można uniknąć pogorszenia wydajności poprzez aktualizację liczby referencji w kodzie krytycznym dla wydajności poprzez trzymanie referencji do obiektu na najwyższym poziomie i przekazywanie bezpośrednich referencji do obiektu.

Jeśli potrzebujesz współwłasności, ale nie chcesz ponosić kosztów zliczania referencji lub wyrzucania elementów bezużytecznych, rozważ użycie niezmiennych obiektów lub kopii przy idiomie zapisu.

Pamiętaj, że zdecydowanie największe wygrane w wydajności będą na poziomie architektury, a następnie na poziomie algorytmu i chociaż te obawy dotyczące niskiego poziomu są bardzo ważne, należy je rozwiązać dopiero po rozwiązaniu głównych problemów. Jeśli masz do czynienia z problemami z wydajnością na poziomie braków w pamięci podręcznej, masz cały szereg problemów, o których musisz pamiętać, podobnie jak fałszywe udostępnianie, które nie mają nic wspólnego ze wskaźnikami na słowo.

Jeśli używasz inteligentnych wskaźników tylko do udostępniania zasobów, takich jak tekstury lub modele, rozważ bardziej specjalistyczną bibliotekę, taką jak Boost.Flyweight.

Gdy nowy standard zostanie przyjęty, semantyka ruchu, odwołania do wartości i doskonałe przekazywanie sprawią, że praca z drogimi obiektami i kontenerami będzie znacznie łatwiejsza i bardziej wydajna. Do tego czasu nie przechowuj wskaźników z destrukcyjną semantyką kopiowania, takich jak auto_ptr lub unique_ptr, w kontenerze (koncepcja standardowa). Rozważ użycie biblioteki Boost.Pointer Container lub przechowywanie inteligentnych wskaźników wspólnych własności w kontenerach. W kodzie krytycznym pod względem wydajności można rozważyć uniknięcie obu z nich na rzecz natrętnych pojemników, takich jak te w Boost.

Platforma docelowa nie powinna zbytnio wpływać na twoją decyzję. Urządzenia wbudowane, smartfony, głupie telefony, komputery PC i konsole mogą dobrze działać z kodem. Wymagania projektu, takie jak ścisłe budżety pamięci lub brak dynamicznej alokacji zawsze / po załadowaniu, są ważniejsze i powinny wpływać na twoje wybory.

Jaeger
źródło
3
Obsługa wyjątków na konsolach może być nieco podejrzana - w szczególności XDK jest wyjątkowo wrogi.
Crashworks,
1
Platforma docelowa naprawdę powinna wpływać na Twój projekt. Sprzęt, który przekształca dane, może czasami mieć duży wpływ na kod źródłowy. Architektura PS3 to konkretny przykład, w którym naprawdę potrzebujesz sprzętu do projektowania zasobów i zarządzania pamięcią, a także renderera.
Simon
Nie zgadzam się tylko nieznacznie, szczególnie w odniesieniu do GC. W większości przypadków odniesienia cykliczne nie stanowią problemu dla schematów zliczanych według odniesień. Zasadniczo pojawiają się te cykliczne problemy z własnością, ponieważ ludzie źle myśleli o własności przedmiotów. To, że obiekt musi wskazywać na coś, nie oznacza, że ​​powinien być właścicielem tego wskaźnika. Często cytowanym przykładem są tylne wskaźniki na drzewach, ale rodzic nad wskaźnikiem na drzewie może bezpiecznie być surowym wskaźnikiem bez poświęcania bezpieczeństwa.
Tim Seguine,
4

Jeśli używasz C ++ 0x, użyj std::unique_ptr<T>.

Nie ma narzutu wydajności, w przeciwieństwie do tego, std::shared_ptr<T>który ma narzut zliczania referencji. Unique_ptr jest właścicielem wskaźnika i możesz przenosić własność dzięki semantyce ruchów C ++ 0x . Nie możesz ich skopiować - tylko je przenieś.

Może być również stosowany w kontenerach, np. std::vector<std::unique_ptr<T>>Kompatybilnych binarnie i identycznych pod względem wydajności std::vector<T*>, ale nie spowoduje przecieku pamięci, jeśli usuniesz elementy lub usuniesz wektor. Ma to również lepszą kompatybilność z algorytmami STL niż ptr_vector.

IMO do wielu celów jest to idealny pojemnik: losowy dostęp, wyjątek bezpieczny, zapobiega wyciekom pamięci, niski narzut dla realokacji wektorów (tylko tasuje wokół wskaźników za sceną). Bardzo przydatny do wielu celów.

AshleysBrain
źródło
3

Dobrą praktyką jest dokumentowanie, które klasy są właścicielami jakich wskaźników. Najlepiej jest używać zwykłych obiektów i żadnych wskaźników, kiedy tylko możesz.

Jednak gdy musisz śledzić zasoby, przekazywanie wskaźników jest jedyną opcją. Istnieje kilka przypadków:

  • Wskaźnik dostajesz gdzie indziej, ale nie zarządzaj nim: po prostu użyj normalnego wskaźnika i udokumentuj go, aby żaden programista nie próbował go usunąć.
  • Otrzymujesz wskaźnik gdzieś indziej i śledzisz go: użyj scoped_ptr.
  • Otrzymujesz wskaźnik gdzieś indziej i śledzisz go, ale potrzebuje specjalnej metody, aby go usunąć: użyj shared_ptr z niestandardową metodą usuwania.
  • Potrzebujesz wskaźnika w kontenerze STL: zostanie on skopiowany, więc potrzebujesz boost :: shared_ptr.
  • Wiele klas ma wspólny wskaźnik i nie jest jasne, kto go usunie: shared_ptr (powyższy przypadek jest w rzeczywistości szczególnym przypadkiem tego punktu).
  • Wskaźnik sam tworzysz i tylko go potrzebujesz: jeśli naprawdę nie możesz użyć normalnego obiektu: scoped_ptr.
  • Utworzysz wskaźnik i udostępnisz go innym klasom: shared_ptr.
  • Tworzysz wskaźnik i przekazujesz go: użyj normalnego wskaźnika i udokumentuj swój interfejs, aby nowy właściciel wiedział, że sam powinien zarządzać zasobem!

Myślę, że to w dużej mierze dotyczy tego, jak teraz zarządzam swoimi zasobami. Koszt pamięci wskaźnika, takiego jak shared_ptr, jest na ogół dwa razy większy niż koszt pamięci normalnego wskaźnika. Nie sądzę, że narzut ten jest zbyt duży, ale jeśli masz mało zasobów, powinieneś rozważyć zaprojektowanie swojej gry w celu zmniejszenia liczby inteligentnych wskaźników. W innych przypadkach po prostu projektuję według dobrych zasad, takich jak powyższe kule, a profiler powie mi, gdzie będę potrzebować większej prędkości.

Nef
źródło
1

Jeśli chodzi o wskaźniki doładowania, uważam, że należy ich unikać, o ile ich wdrożenie nie jest dokładnie tym, czego potrzebujesz. Są dostępne po koszcie większym niż ktokolwiek początkowo się spodziewał. Zapewniają interfejs, który pozwala pominąć ważne i ważne części zarządzania pamięcią i zasobami.

Jeśli chodzi o rozwój oprogramowania, myślę, że ważne jest, aby myśleć o swoich danych. Bardzo ważne jest, w jaki sposób dane są reprezentowane w pamięci. Powodem tego jest to, że szybkość procesora rośnie znacznie szybciej niż czas dostępu do pamięci. To często czyni pamięć podręczną głównym wąskim gardłem większości nowoczesnych gier komputerowych. Dostosowanie danych do pamięci liniowej zgodnie z kolejnością dostępu jest o wiele bardziej przyjazne dla pamięci podręcznej. Tego rodzaju rozwiązania często prowadzą do czystszych projektów, prostszego kodu i zdecydowanie kodu, który jest łatwiejszy do debugowania. Inteligentne wskaźniki łatwo prowadzą do częstych dynamicznych alokacji pamięci w zasobach, co powoduje, że są one rozproszone po całej pamięci.

To nie jest przedwczesna optymalizacja, to zdrowa decyzja, którą można i należy podjąć jak najwcześniej. Jest to kwestia architektonicznego zrozumienia sprzętu, na którym będzie działać twoje oprogramowanie, i jest to ważne.

Edycja: Istnieje kilka rzeczy, które należy wziąć pod uwagę w odniesieniu do wydajności wspólnych wskaźników:

  • Licznik referencyjny jest przydzielany do sterty.
  • Jeśli korzystasz z włączonego zabezpieczenia wątków, liczenie referencji odbywa się za pomocą powiązanych operacji.
  • Przekazywanie wskaźnika przez wartość modyfikuje licznik referencji, co oznacza, że ​​operacje zablokowane najprawdopodobniej wykorzystują losowy dostęp do pamięci (blokady + prawdopodobne pominięcie pamięci podręcznej).
Simon
źródło
2
Straciłeś mnie przy „unikaniu za wszelką cenę”. Następnie opisz rodzaj optymalizacji, która rzadko stanowi problem w grach rzeczywistych. Większość gier charakteryzuje się problemami programistycznymi (opóźnienia, błędy, gry itp.), A nie brakiem wydajności pamięci podręcznej procesora. Dlatego zdecydowanie nie zgadzam się z pomysłem, że ta rada nie jest przedwczesną optymalizacją.
kevin42,
2
Muszę się zgodzić z wczesnym zaprojektowaniem układu danych. Ważne jest, aby uzyskać wydajność z nowoczesnej konsoli / urządzenia mobilnego i jest to coś, czego nigdy nie należy przeoczyć.
Olly,
1
Jest to problem, który widziałem w jednym ze studiów AAA, nad którym pracowałem. Możesz także posłuchać dyrektora naczelnego Insomniac Games, Mike'a Actona. Nie twierdzę, że boost jest złą biblioteką, nie nadaje się tylko do wysokowydajnych gier.
Simon
1
@ kevin42: Spójność pamięci podręcznej jest obecnie prawdopodobnie głównym źródłem optymalizacji niskiego poziomu w rozwoju gier. @ Simon: Większość implementacji shared_ptr unika blokad na dowolnej platformie obsługującej porównywanie i zamianę, która obejmuje komputery z systemem Linux i Windows, i uważam, że obejmuje Xbox.
1
@Joe Wreschnig: To prawda, brak pamięci podręcznej jest nadal najbardziej prawdopodobny, chociaż powoduje jakąkolwiek inicjalizację wskaźnika wspólnego (kopiowanie, tworzenie ze słabego wskaźnika itp.). Brak pamięci podręcznej L2 na nowoczesnych komputerach to około 200 cykli, a na PPC (xbox360 / ps3) jest wyższy. W intensywnej grze możesz mieć do 1000 obiektów gry, biorąc pod uwagę, że każdy obiekt gry może mieć całkiem sporo zasobów, a my przyglądamy się problemom, w których ich skalowanie jest głównym problemem. Prawdopodobnie spowoduje to problemy na końcu cyklu rozwoju (kiedy trafisz dużą liczbę obiektów w grze).
Simon
0

Zwykle używam inteligentnych wskaźników wszędzie. Nie jestem pewien, czy to jest całkiem dobry pomysł, ale jestem leniwy i nie widzę żadnego prawdziwego minusa [z wyjątkiem tego, że chciałbym wykonać arytmetykę wskaźnika w stylu C]. Używam boost :: shared_ptr, ponieważ wiem, że mogę go skopiować - jeśli dwa byty udostępniają obraz, to jeśli jedno umiera, drugie też nie powinno go utracić.

Minusem tego jest to, że jeśli jeden obiekt usuwa coś, na co wskazuje i jest właścicielem, ale coś innego wskazuje na to, to nie jest usuwane.

Kaczka komunistyczna
źródło
1
Korzystam z share_ptr także prawie wszędzie - ale dzisiaj próbuję się zastanowić, czy rzeczywiście potrzebuję współwłasności dla niektórych danych. Jeśli nie, uzasadnione może być uczynienie tych danych elementem niebędącym wskaźnikiem w strukturze danych nadrzędnych. Uważam, że jasne prawo własności upraszcza projekty.
jmp97
0

Korzyści z zarządzania pamięcią i dokumentacji zapewniane przez dobre inteligentne wskaźniki oznaczają, że używam ich regularnie. Jednak gdy profiler wypowiada się i mówi mi, że szczególne użycie mnie kosztuje, wrócę do bardziej neolitycznego zarządzania wskaźnikami.

tenpn
źródło
0

Jestem stary, oldskool i licznik cykli. W mojej własnej pracy używam surowych wskaźników i brak dynamicznych alokacji w czasie wykonywania (z wyjątkiem samych pul). Wszystko jest połączone, a własność jest bardzo rygorystyczna i nigdy nie można jej przenieść, w razie potrzeby piszę niestandardowy alokator małych bloków. Upewniam się, że podczas gry istnieje stan, w którym każda pula może się wyczyścić. Kiedy rzeczy stają się owłosione, zawijam przedmioty w uchwyty, aby móc je przenieść, ale wolałbym nie. Pojemniki to niestandardowe i wyjątkowo gołe kości. Nie używam też kodu ponownie.
Chociaż nigdy nie dyskutowałbym na temat zalet wszystkich inteligentnych wskaźników, kontenerów i iteratorów, i tak dalej, jestem znany z tego, że potrafię kodować bardzo szybko (i względnie niezawodnie - chociaż nie jest wskazane, aby inni wskakiwali do mojego kodu z dość oczywistych powodów, jak zawały serca i wieczne koszmary).

W pracy oczywiście wszystko jest inne, chyba że wykonuję prototypy, co na szczęście mogę dużo zrobić.

Kaj
źródło
0

Prawie żadna nie jest to wprawdzie dziwna odpowiedź i prawdopodobnie nigdzie nie odpowiednia dla wszystkich.

Jednak w moim osobistym przypadku znacznie bardziej przydało mi się przechowywanie wszystkich instancji określonego typu w centralnej sekwencji o swobodnym dostępie (bezpieczny dla wątków), a zamiast tego praca z indeksami 32-bitowymi (adresy względne, tj.) , zamiast wskaźników bezwzględnych.

Na początek:

  1. Zmniejsza o połowę wymagania dotyczące pamięci wskaźnika analogowego na platformach 64-bitowych. Do tej pory nigdy nie potrzebowałem więcej niż ~ 4,29 miliarda instancji określonego typu danych.
  2. Daje to pewność, że wszystkie wystąpienia określonego typu Tnigdy nie będą zbyt rozproszone w pamięci. Ma to tendencję do zmniejszania liczby braków w pamięci podręcznej dla wszystkich rodzajów wzorców dostępu, nawet przemierzając połączone struktury, takie jak drzewa, jeśli węzły są połączone za pomocą indeksów zamiast wskaźników.
  3. Dane równoległe stają się łatwe do kojarzenia za pomocą tanich tablic równoległych (lub tablic rzadkich) zamiast drzew lub tabel mieszania.
  4. Zestaw przecięć można znaleźć w czasie liniowym lub lepiej, używając, powiedzmy, równoległego zestawu bitów.
  5. Możemy porządkować indeksy i uzyskać bardzo przyjazny dla pamięci podręcznej wzorzec sekwencyjnego dostępu.
  6. Możemy śledzić, ile wystąpień przypisano do określonego typu danych.
  7. Minimalizuje liczbę miejsc, które mają do czynienia z takimi sprawami, jak bezpieczeństwo wyjątkowe, jeśli zależy Ci na takich rzeczach.

To powiedziawszy, wygoda jest wadą, podobnie jak bezpieczeństwo typu. Nie możemy uzyskać dostępu do instancji Tbez dostępu zarówno do kontenera, jak i indeksu. A zwykły stary nie int32_tmówi nam nic o tym, do jakiego typu danych się odnosi, więc nie ma bezpieczeństwa typu. Możemy przypadkowo spróbować uzyskać dostęp Bardo indeksu za pomocą Foo. Aby złagodzić drugi problem, często robię takie rzeczy:

struct FooIndex
{
    int32_t index;
};

To wydaje się trochę głupie, ale przywraca mi bezpieczeństwo typu, dzięki czemu ludzie nie mogą przypadkowo próbować uzyskać dostępu do Barindeksu Foobez błędu kompilatora. Jeśli chodzi o wygodę, akceptuję niewielką niedogodność.

Inną rzeczą, która może być poważną niedogodnością dla ludzi, jest to, że nie mogę używać polimorfizmu opartego na dziedziczeniu opartym na OOP, ponieważ wymagałoby to wskaźnika bazowego, który wskazywałby na różnego rodzaju podtypy o różnych wymaganiach dotyczących wielkości i wyrównania. Ale ostatnio nie używam dziedziczenia - wolę podejście ECS.

Jeśli chodzi o to shared_ptr, staram się nie używać go tak często. Przez większość czasu nie wydaje mi się, że dzielenie się własnością ma sens, a robienie tego w sposób przypadkowy może doprowadzić do logicznych wycieków. Często przynajmniej na wysokim poziomie jedna rzecz zwykle należy do jednej rzeczy. To, co często uważałem za kuszące, shared_ptrto przedłużanie żywotności obiektu w miejscach, które tak naprawdę nie zajmowały się tak bardzo własnością, jak tylko lokalna funkcja w wątku, aby upewnić się, że obiekt nie zostanie zniszczony przed zakończeniem wątku Użyj tego.

Aby rozwiązać ten problem, zamiast używania shared_ptrGC lub czegoś podobnego, często preferuję krótkotrwałe zadania uruchamiane z puli wątków i robię to tak, jeśli ten wątek prosi o zniszczenie obiektu, że faktyczne zniszczenie jest odkładane do bezpiecznego czas, w którym system może zapewnić, że żaden wątek nie musi uzyskać dostępu do tego typu obiektu.

Nadal czasami używam liczenia odwołań, ale traktuję to jak strategię ostateczności. I jest kilka przypadków, w których naprawdę sensowne jest dzielenie się własnością, na przykład wdrożenie trwałej struktury danych, i tam wydaje mi się, że warto sięgać od shared_ptrrazu.

Tak czy inaczej, najczęściej używam indeksów i oszczędnie używam zarówno surowych, jak i inteligentnych wskaźników. Lubię indeksy i rodzaje drzwi, które otwierają, gdy wiesz, że twoje obiekty są przechowywane w sposób ciągły, a nie rozrzucone po przestrzeni pamięci.

user77245
źródło