Майже ніхто, хоча це, правда, дивна відповідь, і, мабуть, ніде не підходить для всіх.
Але в моєму особистому випадку я виявився набагато кориснішим для зберігання всіх примірників певного типу в центральній послідовності з випадковим доступом (безпечна для потоків) і замість цього працювати з 32-бітовими індексами (відносні адреси, тобто) , а не абсолютні покажчики.
Для початку:
- Це вдвічі зменшує вимоги до пам'яті аналогічного вказівника на 64-бітних платформах. Поки мені ніколи не було потрібно більше ~ 4,29 мільярдів екземплярів певного типу даних.
- Це гарантує, що всі екземпляри певного типу
T
ніколи не будуть надто розсіяні в пам'яті. Це, як правило, зменшує пропуски кеш-пам'яті для всіх видів шаблонів доступу, навіть переходячи пов'язані структури, як дерева, якщо вузли пов'язані між собою за допомогою індексів, а не покажчиків.
- Паралельні дані стає легко асоціюватися, використовуючи дешеві паралельні масиви (або розріджені масиви) замість дерев або хеш-таблиць.
- Встановлені перехрестя можна знайти в лінійному часі або краще використовувати, скажімо, паралельний біт.
- Ми можемо радіаційно сортувати індекси та отримати дуже зручний кеш-схему послідовного доступу.
- Ми можемо відслідковувати, скільки примірників виділили певний тип даних.
- Мінімізує кількість місць, які стосуються таких речей, як безпека винятків, якщо ви дбаєте про такі речі.
Однак, зручність є як недоліком, так і безпекою типу. Ми не можемо отримати доступ до примірника , T
не маючи доступу до як контейнер і індекс. А звичайний старий int32_t
нічого не говорить про те, до якого типу даних вони відносяться, тому немає безпеки типу. Ми могли випадково спробувати отримати доступ Bar
до індексу за допомогою Foo
. Щоб пом'якшити другу проблему, я часто роблю такі речі:
struct FooIndex
{
int32_t index;
};
Що здається дурним, але це повертає мені безпеку типу, так що люди не можуть випадково спробувати отримати доступ Bar
через індекс Foo
без помилки компілятора. Для зручності я просто приймаю невеликі незручності.
Інша річ, яка може бути великою незручністю для людей, - це те, що я не можу використовувати поліморфізм, заснований на спадщині в стилі OOP, оскільки це вимагатиме базового вказівника, який може вказувати на всі види різних підтипів з різними вимогами до розміру та вирівнювання. Але я не користуюся спадщиною в наші дні - віддаю перевагу підходу ECS.
Що стосується shared_ptr
, я намагаюся не користуватися ним так сильно. Більшу частину часу я не вважаю, що має сенс розділяти право власності, і це робити випадково може призвести до логічних витоків. Часто хоча б на високому рівні одна річ, як правило, належить до однієї речі. Де я часто вважаю заманливим використовувати shared_ptr
продовження терміну експлуатації об’єкта в місцях, які не так сильно займаються правом власності, як просто локальна функція в потоці, щоб переконатися, що об'єкт не знищений до завершення потоку. використовуючи його.
Щоб вирішити цю проблему, замість того, щоб використовувати shared_ptr
або GC чи щось подібне, я часто віддаю перевагу короткочасним завданням, що працюють із пулу потоків, і роблю це, якщо цей потік вимагає знищити об'єкт, що фактичне знищення переноситься на безпечний час, коли система може гарантувати, що жоден потік не потребує доступу до вказаного типу об’єкта.
Я все ще іноді в кінцевому підсумку використовую перерахунок, але трактую це як стратегію в крайньому випадку. І є кілька випадків, коли справді має сенс розділити право власності, як, наприклад, реалізація стійкої структури даних, і там я вважаю, що має сенс звернутися до них shared_ptr
відразу.
Отже, я в основному використовую індекси, і в основному економно використовую як сирі, так і розумні покажчики. Мені подобаються показники та види дверей, які вони відкриваються, коли ви знаєте, що ваші предмети зберігаються постійно, а не розкидані по пам’яті.