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:
- 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.
- Daje to pewność, że wszystkie wystąpienia określonego typu
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.
- Dane równoległe stają się łatwe do kojarzenia za pomocą tanich tablic równoległych (lub tablic rzadkich) zamiast drzew lub tabel mieszania.
- Zestaw przecięć można znaleźć w czasie liniowym lub lepiej, używając, powiedzmy, równoległego zestawu bitów.
- Możemy porządkować indeksy i uzyskać bardzo przyjazny dla pamięci podręcznej wzorzec sekwencyjnego dostępu.
- Możemy śledzić, ile wystąpień przypisano do określonego typu danych.
- 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 T
bez dostępu zarówno do kontenera, jak i indeksu. A zwykły stary nie int32_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ęp Bar
do 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 Bar
indeksu Foo
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.