Почти ничего, хотя это, по общему признанию, странный ответ, и, вероятно, нигде не подходит для всех.
Но я обнаружил, что в моем личном случае гораздо полезнее хранить все экземпляры определенного типа в центральной последовательности с произвольным доступом (поточно-ориентированной) и вместо этого работать с 32-разрядными индексами (относительными адресами, т.е.). , а не абсолютные указатели.
Для начала:
- Это вдвое уменьшает требования к памяти для аналогового указателя на 64-битных платформах. До сих пор мне никогда не требовалось более ~ 4,29 миллиарда экземпляров определенного типа данных.
- Это гарантирует, что все экземпляры определенного типа,
T
никогда не будут слишком разбросаны в памяти. Это имеет тенденцию уменьшать потери кэша для всех типов шаблонов доступа, даже обходя связанные структуры, такие как деревья, если узлы связаны друг с другом, используя индексы, а не указатели.
- Параллельные данные легко связать, используя дешевые параллельные массивы (или разреженные массивы) вместо деревьев или хеш-таблиц.
- Набор пересечений может быть найден в линейном времени или лучше, например, с использованием параллельного набора битов.
- Мы можем радикально отсортировать индексы и получить очень удобный кеш-шаблон последовательного доступа.
- Мы можем отслеживать, сколько экземпляров того или иного типа данных было выделено.
- Минимизирует количество мест, где приходится иметь дело с такими вещами, как безопасность исключений, если вы заботитесь о таких вещах.
Тем не менее, удобство является недостатком, а также безопасность типов. Мы не можем получить доступ к экземпляру , T
не имея доступа к как контейнер и индекс. А старый добрый старый int32_t
нам ничего не говорит о том, к какому типу данных он относится, поэтому нет безопасности типов. Мы могли бы случайно попытаться получить доступ Bar
к индексу Foo
. Чтобы смягчить вторую проблему, я часто делаю такие вещи:
struct FooIndex
{
int32_t index;
};
Что кажется глупым, но возвращает мне безопасность типов, так что люди не могут случайно попытаться получить доступ Bar
через индекс Foo
без ошибки компилятора. Для удобства я просто принимаю небольшие неудобства.
Еще одна вещь, которая может быть большим неудобством для людей, заключается в том, что я не могу использовать полиморфизм на основе наследования в стиле ООП, поскольку для этого потребуется базовый указатель, который может указывать на все виды различных подтипов с различными размерами и требованиями выравнивания. Но я не очень часто использую наследование - предпочитаю подход ECS.
Что касается shared_ptr
, я стараюсь не использовать это так много. Большую часть времени я не нахожу смысла делиться собственностью, и это может привести к логическим утечкам. Часто, по крайней мере на высоком уровне, одна вещь имеет тенденцию принадлежать одной вещи. Там, где мне часто shared_ptr
приходилось заманчиво использовать, было продление срока службы объекта в местах, где на самом деле не так много внимания уделялось владению, как просто в локальной функции в потоке, чтобы убедиться, что объект не уничтожен до завершения потока используй это.
Чтобы решить эту проблему, вместо использования shared_ptr
или GC или чего-то подобного, я часто отдаю предпочтение краткосрочным задачам, выполняющимся из пула потоков, и делаю так, чтобы, если этот поток запрашивал уничтожение объекта, фактическое уничтожение было отложено до безопасного время, когда система может гарантировать, что никакой поток не должен обращаться к указанному типу объекта.
Я все еще иногда заканчиваю тем, что использую пересчет, но рассматриваю это как стратегию последней инстанции. И есть несколько случаев, когда действительно имеет смысл разделять владение, например, внедрение постоянной структуры данных, и я считаю, что это имеет смысл сразу же достичь shared_ptr
.
Так или иначе, я в основном использую индексы и редко использую как сырые, так и умные указатели. Мне нравятся индексы и виды дверей, которые они открывают, когда вы знаете, что ваши объекты хранятся непрерывно, а не разбросаны по пространству памяти.