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
źródło
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_ptr
kiedy tylko mogę, więc mogę traktować obiekt jak uchwyt obiektu zamiast zwiększać liczbę referencji.Używam
scoped_ptr
wszę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żywanievector<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
delete
połą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.
źródło
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.
źródło
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ścistd::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.
źródło
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:
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.
źródło
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:
źródło
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.
źródło
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.
źródło
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ć.
źródło
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:
T
nigdy 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.To powiedziawszy, wygoda jest wadą, podobnie jak bezpieczeństwo typu. Nie możemy uzyskać dostępu do instancji
T
bez dostępu zarówno do kontenera, jak i indeksu. A zwykły stary nieint32_t
mó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ępBar
do indeksu za pomocąFoo
. Aby złagodzić drugi problem, często robię takie rzeczy: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
Bar
indeksuFoo
bez 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_ptr
to 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_ptr
GC 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_ptr
razu.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.
źródło