В рамках разработки игр на C ++, каковы ваши предпочтения в отношении использования указателей (будь то ни один, необработанный, ограниченный, совместно используемый или иным образом между умными и глупыми)?
Вы могли бы рассмотреть
- владение объектом
- простота использования
- политика копирования
- накладные расходы
- циклические ссылки
- целевая платформа
- использовать с контейнерами
источник
Я также следую за ходом «сильной собственности». Мне нравится четко обозначать, что «этот класс владеет этим членом», когда это уместно.
Я редко использую
shared_ptr
. Если я это сделаю, я свободно использую все,weak_ptr
что могу, поэтому я могу обращаться с ним как с ручкой к объекту, а не увеличивать счетчик ссылок.Я использую
scoped_ptr
повсюду. Это показывает очевидную собственность. Единственная причина, по которой я не просто делаю объекты подобными этому, состоит в том, что вы можете объявить их вперед, если они находятся в scoped_ptr.Если мне нужен список объектов, я использую
ptr_vector
. Это более эффективно и имеет меньше побочных эффектов, чем при использованииvector<shared_ptr>
. Я думаю, что вы не сможете перенаправить объявление типа в ptr_vector (это было давно), но семантика этого стоит того, по моему мнению. Обычно, если вы удаляете объект из списка, он автоматически удаляется. Это также показывает очевидную собственность.Если мне нужна ссылка на что-то, я пытаюсь сделать это ссылкой вместо голого указателя. Иногда это не практично (т. Е. В любое время, когда вам нужна ссылка после того, как объект создан). В любом случае, ссылки показывают, очевидно, что вы не являетесь владельцем объекта, и если вы везде следуете семантике общих указателей, то голые указатели, как правило, не вызывают дополнительной путаницы (особенно если вы следуете правилу «не удаляются вручную») ,
С помощью этого метода одна игра для iPhone, над которой я работал, могла иметь только один
delete
вызов, и это было в мосте Obj-C на C ++, который я написал.Обычно я придерживаюсь мнения, что управление памятью слишком важно, чтобы оставлять его людям. Если вы можете автоматизировать удаление, вы должны. Если накладные расходы от shared_ptr слишком дороги во время выполнения (при условии, что вы отключили поддержку многопоточности и т. Д.), Вам, вероятно, следует использовать что-то другое (например, шаблон сегмента), чтобы сократить динамическое распределение.
источник
Используйте правильный инструмент для работы.
Если ваша программа может генерировать исключения, убедитесь, что ваш код осведомлен об исключениях. Использование умных указателей, RAII и избегание двухфазного построения - хорошие отправные точки.
Если у вас есть циклические ссылки без четкой семантики владения, вы можете рассмотреть возможность использования библиотеки сборки мусора или рефакторинга вашего дизайна.
Хорошие библиотеки позволят вам кодировать концепцию, а не тип, поэтому в большинстве случаев не должно иметь значения, какой тип указателя вы используете, кроме вопросов управления ресурсами.
Если вы работаете в многопоточной среде, убедитесь, что вы понимаете, является ли ваш объект потенциально общим для потоков. Одна из основных причин, по которой стоит рассмотреть возможность использования boost :: shared_ptr или std :: tr1 :: shared_ptr, заключается в том, что он использует потокобезопасный счетчик ссылок.
Если вы беспокоитесь о раздельном распределении количества ссылок, есть много способов обойти это. Используя библиотеку boost :: shared_ptr, вы можете объединить счетчики ссылок в пул или использовать boost :: make_shared (мои предпочтения), который выделяет объект и счетчик ссылок в одном выделении, тем самым устраняя большинство проблем, связанных с отсутствием кэша, которые есть у людей. Вы можете избежать снижения производительности путем обновления счетчика ссылок в критическом для производительности коде, удерживая ссылку на объект на самом верхнем уровне и передавая прямые ссылки на объект.
Если вам нужно совместное владение, но вы не хотите платить за подсчет ссылок или сборку мусора, подумайте об использовании неизменяемых объектов или идиомы копирования при записи.
Имейте в виду, что ваши самые большие выигрыши в производительности будут происходить на уровне архитектуры, за которым следует уровень алгоритма, и хотя эти проблемы низкого уровня очень важны, их следует решать только после того, как вы решите основные проблемы. Если вы сталкиваетесь с проблемами производительности на уровне пропусков кэша, у вас есть целый ряд проблем, о которых вам также следует знать, например, ложное совместное использование, которое не имеет ничего общего с указателями на одно слово.
Если вы используете умные указатели просто для совместного использования ресурсов, таких как текстуры или модели, рассмотрите более специализированную библиотеку, такую как Boost.Flyweight.
Как только новый стандарт станет общепринятым, семантика перемещения, ссылки на значения и совершенная пересылка сделают работу с дорогими объектами и контейнерами намного проще и эффективнее. До тех пор не храните указатели с разрушительной семантикой копирования, такой как auto_ptr или unique_ptr, в контейнере (стандартная концепция). Рассмотрите возможность использования библиотеки контейнеров Boost.Pointer или хранения интеллектуальных указателей общего владения в контейнерах. В коде, критичном к производительности, вы можете избегать их использования в пользу навязчивых контейнеров, таких как в Boost.Intrusive.
Целевая платформа не должна сильно влиять на ваше решение. Встроенные устройства, смартфоны, обычные телефоны, ПК и консоли могут прекрасно выполнять код. Требования проекта, такие как строгий бюджет памяти или отсутствие динамического выделения памяти когда-либо / после загрузки, являются более актуальными проблемами и должны влиять на ваш выбор.
источник
Если вы используете C ++ 0x, используйте
std::unique_ptr<T>
.Он не имеет никаких накладных расходов производительности, в отличие от
std::shared_ptr<T>
которых накладные расходы подсчета ссылок. Свойство unique_ptr владеет указателем, и вы можете передавать владение с помощью семантики перемещения C ++ 0x . Вы не можете скопировать их - только переместите их.Он также может использоваться в контейнерах, например
std::vector<std::unique_ptr<T>>
, которые являются двоично-совместимыми и идентичными по производительностиstd::vector<T*>
, но не будут пропускать память, если вы удалите элементы или очистите вектор. Это также имеет лучшую совместимость с алгоритмами STL, чемptr_vector
.IMO для многих целей это идеальный контейнер: произвольный доступ, безопасное исключение, предотвращение утечек памяти, низкие накладные расходы для перераспределения векторов (просто перетасовывает указатели за кулисы). Очень полезно для многих целей.
источник
Хорошей практикой является документирование того, какие классы владеют указателями. Желательно, чтобы вы просто использовали обычные объекты и не указывали, когда бы ни могли.
Однако, когда вам нужно отслеживать ресурсы, передача указателей является единственной возможностью. Есть несколько случаев:
Я думаю, что это в значительной степени охватывает то, как я управляю своими ресурсами прямо сейчас. Стоимость памяти для указателя, такого как shared_ptr, обычно вдвое больше, чем для обычного указателя. Я не думаю, что эти издержки слишком велики, но если у вас мало ресурсов, вам следует подумать о разработке своей игры, чтобы уменьшить количество умных указателей. В других случаях я просто создаю хорошие принципы, такие как приведенные выше, и профилировщик скажет мне, где мне понадобится больше скорости.
источник
Что касается конкретно указателей boost, я думаю, что их следует избегать, если их реализация не совсем то, что вам нужно. Они приходят по цене, которая больше, чем кто-либо первоначально ожидал. Они предоставляют интерфейс, который позволяет вам пропустить важные и важные части вашей памяти и управления ресурсами.
Когда дело доходит до разработки программного обеспечения, я думаю, что важно думать о ваших данных. Очень важно, как ваши данные представлены в памяти. Причина этого заключается в том, что скорость процессора увеличивается гораздо быстрее, чем время доступа к памяти. Это часто делает кэши памяти главным узким местом большинства современных компьютерных игр. Линейное выравнивание данных в памяти в соответствии с порядком доступа намного удобнее для кэша. Такие решения часто приводят к более чистым проектам, более простому коду и определенно коду, который легче отлаживать. Умные указатели легко приводят к частому динамическому распределению ресурсов в памяти, что приводит к их разбросу по всей памяти.
Это не преждевременная оптимизация, это здоровое решение, которое можно и нужно принимать как можно раньше. Это вопрос архитектурного понимания оборудования, на котором будет работать ваше программное обеспечение, и это важно.
Изменить: Есть несколько вещей, которые следует учитывать относительно производительности общих указателей:
источник
Я склонен использовать умные указатели везде. Я не уверен, является ли это абсолютно хорошей идеей, но я ленивый, и я не вижу никакого реального недостатка [за исключением того, что я хотел сделать некоторую арифметику указателя в стиле C]. Я использую boost :: shared_ptr, потому что я знаю, что могу скопировать его - если два объекта совместно используют изображение, то, если один из них умирает, другой не должен потерять изображение.
Недостатком этого является то, что если один объект удаляет то, на что он указывает и владеет, но что-то еще указывает на него, он не удаляется.
источник
Преимущества управления памятью и документация, предоставляемые хорошими умными указателями, означают, что я использую их регулярно. Однако, когда профилировщик подключается к сети и сообщает мне, что конкретное использование обходится мне дорого, я вернусь к более неолитическому управлению указателями.
источник
Я старый, oldskool и счетчик циклов. В своей работе я использую сырые указатели и не динамические выделения во время выполнения (кроме самих пулов). Все объединено, и владение очень строго и никогда не может передаваться, если действительно необходимо, я пишу собственный распределитель небольших блоков. Я удостоверяюсь, что во время игры есть состояние, чтобы каждый пул мог очистить себя. Когда вещи становятся волосатыми, я заворачиваю предметы в ручки, чтобы переместить их, но я бы предпочел этого не делать. Контейнеры изготовлены на заказ и чрезвычайно голые. Я также не использую код повторно.
Хотя я бы никогда не стал спорить о достоинствах всех умных указателей, контейнеров, итераторов и так далее, я известен тем, что умею кодировать очень быстро (и достаточно надежно), хотя другим не рекомендуется прыгать в мой код по несколько очевидным причинам, как сердечные приступы и вечные кошмары).
На работе, конечно, все по-другому, если только я не занимаюсь прототипированием, которое я, к счастью, много делаю.
источник
Почти ничего, хотя это, по общему признанию, странный ответ, и, вероятно, нигде не подходит для всех.
Но я обнаружил, что в моем личном случае гораздо полезнее хранить все экземпляры определенного типа в центральной последовательности с произвольным доступом (поточно-ориентированной) и вместо этого работать с 32-разрядными индексами (относительными адресами, т.е.). , а не абсолютные указатели.
Для начала:
T
никогда не будут слишком разбросаны в памяти. Это имеет тенденцию уменьшать потери кэша для всех типов шаблонов доступа, даже обходя связанные структуры, такие как деревья, если узлы связаны друг с другом, используя индексы, а не указатели.Тем не менее, удобство является недостатком, а также безопасность типов. Мы не можем получить доступ к экземпляру ,
T
не имея доступа к как контейнер и индекс. А старый добрый старыйint32_t
нам ничего не говорит о том, к какому типу данных он относится, поэтому нет безопасности типов. Мы могли бы случайно попытаться получить доступBar
к индексуFoo
. Чтобы смягчить вторую проблему, я часто делаю такие вещи:Что кажется глупым, но возвращает мне безопасность типов, так что люди не могут случайно попытаться получить доступ
Bar
через индексFoo
без ошибки компилятора. Для удобства я просто принимаю небольшие неудобства.Еще одна вещь, которая может быть большим неудобством для людей, заключается в том, что я не могу использовать полиморфизм на основе наследования в стиле ООП, поскольку для этого потребуется базовый указатель, который может указывать на все виды различных подтипов с различными размерами и требованиями выравнивания. Но я не очень часто использую наследование - предпочитаю подход ECS.
Что касается
shared_ptr
, я стараюсь не использовать это так много. Большую часть времени я не нахожу смысла делиться собственностью, и это может привести к логическим утечкам. Часто, по крайней мере на высоком уровне, одна вещь имеет тенденцию принадлежать одной вещи. Там, где мне частоshared_ptr
приходилось заманчиво использовать, было продление срока службы объекта в местах, где на самом деле не так много внимания уделялось владению, как просто в локальной функции в потоке, чтобы убедиться, что объект не уничтожен до завершения потока используй это.Чтобы решить эту проблему, вместо использования
shared_ptr
или GC или чего-то подобного, я часто отдаю предпочтение краткосрочным задачам, выполняющимся из пула потоков, и делаю так, чтобы, если этот поток запрашивал уничтожение объекта, фактическое уничтожение было отложено до безопасного время, когда система может гарантировать, что никакой поток не должен обращаться к указанному типу объекта.Я все еще иногда заканчиваю тем, что использую пересчет, но рассматриваю это как стратегию последней инстанции. И есть несколько случаев, когда действительно имеет смысл разделять владение, например, внедрение постоянной структуры данных, и я считаю, что это имеет смысл сразу же достичь
shared_ptr
.Так или иначе, я в основном использую индексы и редко использую как сырые, так и умные указатели. Мне нравятся индексы и виды дверей, которые они открывают, когда вы знаете, что ваши объекты хранятся непрерывно, а не разбросаны по пространству памяти.
источник