C ++: умные указатели, необработанные указатели, никаких указателей? [закрыто]


48

В рамках разработки игр на C ++, каковы ваши предпочтения в отношении использования указателей (будь то ни один, необработанный, ограниченный, совместно используемый или иным образом между умными и глупыми)?

Вы могли бы рассмотреть

  • владение объектом
  • простота использования
  • политика копирования
  • накладные расходы
  • циклические ссылки
  • целевая платформа
  • использовать с контейнерами

Ответы:


32

Перепробовав различные подходы, сегодня я нахожусь в соответствии с Руководством по стилю Google C ++ :

Если вам действительно нужна семантика указателя, scoped_ptr отлично подходит. Вы должны использовать std :: tr1 :: shared_ptr только в очень специфических условиях, например, когда объекты должны храниться в контейнерах STL. Вы никогда не должны использовать auto_ptr. [...]

Вообще говоря, мы предпочитаем, чтобы мы разрабатывали код с четким владением объектом. Самая чистая собственность на объект достигается использованием объекта непосредственно в качестве поля или локальной переменной, без использования указателей вообще. [..]

Хотя они не рекомендуются, указатели с подсчетом ссылок иногда являются самым простым и элегантным способом решения проблемы.


14
Сегодня вы можете использовать std :: unique_ptr вместо scoped_ptr.
Klaim

24

Я также следую за ходом «сильной собственности». Мне нравится четко обозначать, что «этот класс владеет этим членом», когда это уместно.

Я редко использую shared_ptr. Если я это сделаю, я свободно использую все, weak_ptrчто могу, поэтому я могу обращаться с ним как с ручкой к объекту, а не увеличивать счетчик ссылок.

Я использую scoped_ptrповсюду. Это показывает очевидную собственность. Единственная причина, по которой я не просто делаю объекты подобными этому, состоит в том, что вы можете объявить их вперед, если они находятся в scoped_ptr.

Если мне нужен список объектов, я использую ptr_vector. Это более эффективно и имеет меньше побочных эффектов, чем при использовании vector<shared_ptr>. Я думаю, что вы не сможете перенаправить объявление типа в ptr_vector (это было давно), но семантика этого стоит того, по моему мнению. Обычно, если вы удаляете объект из списка, он автоматически удаляется. Это также показывает очевидную собственность.

Если мне нужна ссылка на что-то, я пытаюсь сделать это ссылкой вместо голого указателя. Иногда это не практично (т. Е. В любое время, когда вам нужна ссылка после того, как объект создан). В любом случае, ссылки показывают, очевидно, что вы не являетесь владельцем объекта, и если вы везде следуете семантике общих указателей, то голые указатели, как правило, не вызывают дополнительной путаницы (особенно если вы следуете правилу «не удаляются вручную») ,

С помощью этого метода одна игра для iPhone, над которой я работал, могла иметь только один deleteвызов, и это было в мосте Obj-C на C ++, который я написал.

Обычно я придерживаюсь мнения, что управление памятью слишком важно, чтобы оставлять его людям. Если вы можете автоматизировать удаление, вы должны. Если накладные расходы от shared_ptr слишком дороги во время выполнения (при условии, что вы отключили поддержку многопоточности и т. Д.), Вам, вероятно, следует использовать что-то другое (например, шаблон сегмента), чтобы сократить динамическое распределение.


1
Отличное резюме. Вы действительно имеете в виду shared_ptr, а не упоминание о smart_ptr?
jmp97

Да, я имел в виду shared_ptr. Я исправлю это.
Тетрад

10

Используйте правильный инструмент для работы.

Если ваша программа может генерировать исключения, убедитесь, что ваш код осведомлен об исключениях. Использование умных указателей, RAII и избегание двухфазного построения - хорошие отправные точки.

Если у вас есть циклические ссылки без четкой семантики владения, вы можете рассмотреть возможность использования библиотеки сборки мусора или рефакторинга вашего дизайна.

Хорошие библиотеки позволят вам кодировать концепцию, а не тип, поэтому в большинстве случаев не должно иметь значения, какой тип указателя вы используете, кроме вопросов управления ресурсами.

Если вы работаете в многопоточной среде, убедитесь, что вы понимаете, является ли ваш объект потенциально общим для потоков. Одна из основных причин, по которой стоит рассмотреть возможность использования boost :: shared_ptr или std :: tr1 :: shared_ptr, заключается в том, что он использует потокобезопасный счетчик ссылок.

Если вы беспокоитесь о раздельном распределении количества ссылок, есть много способов обойти это. Используя библиотеку boost :: shared_ptr, вы можете объединить счетчики ссылок в пул или использовать boost :: make_shared (мои предпочтения), который выделяет объект и счетчик ссылок в одном выделении, тем самым устраняя большинство проблем, связанных с отсутствием кэша, которые есть у людей. Вы можете избежать снижения производительности путем обновления счетчика ссылок в критическом для производительности коде, удерживая ссылку на объект на самом верхнем уровне и передавая прямые ссылки на объект.

Если вам нужно совместное владение, но вы не хотите платить за подсчет ссылок или сборку мусора, подумайте об использовании неизменяемых объектов или идиомы копирования при записи.

Имейте в виду, что ваши самые большие выигрыши в производительности будут происходить на уровне архитектуры, за которым следует уровень алгоритма, и хотя эти проблемы низкого уровня очень важны, их следует решать только после того, как вы решите основные проблемы. Если вы сталкиваетесь с проблемами производительности на уровне пропусков кэша, у вас есть целый ряд проблем, о которых вам также следует знать, например, ложное совместное использование, которое не имеет ничего общего с указателями на одно слово.

Если вы используете умные указатели просто для совместного использования ресурсов, таких как текстуры или модели, рассмотрите более специализированную библиотеку, такую ​​как Boost.Flyweight.

Как только новый стандарт станет общепринятым, семантика перемещения, ссылки на значения и совершенная пересылка сделают работу с дорогими объектами и контейнерами намного проще и эффективнее. До тех пор не храните указатели с разрушительной семантикой копирования, такой как auto_ptr или unique_ptr, в контейнере (стандартная концепция). Рассмотрите возможность использования библиотеки контейнеров Boost.Pointer или хранения интеллектуальных указателей общего владения в контейнерах. В коде, критичном к производительности, вы можете избегать их использования в пользу навязчивых контейнеров, таких как в Boost.Intrusive.

Целевая платформа не должна сильно влиять на ваше решение. Встроенные устройства, смартфоны, обычные телефоны, ПК и консоли могут прекрасно выполнять код. Требования проекта, такие как строгий бюджет памяти или отсутствие динамического выделения памяти когда-либо / после загрузки, являются более актуальными проблемами и должны влиять на ваш выбор.


3
Обработка исключений на консолях может быть немного хитрой - в частности, XDK является своего рода враждебным исключением.
Crashworks

1
Целевая платформа действительно должна влиять на ваш дизайн. Аппаратные средства, которые преобразуют ваши данные, могут иногда иметь большое влияние на ваш исходный код. PS3-архитектура - это конкретный пример, где вам действительно нужно использовать оборудование для проектирования ресурсов, управления памятью и рендерера.
Симон

Я не совсем согласен, особенно в отношении GC. В большинстве случаев циклические ссылки не являются проблемой для схем с подсчетом ссылок. Как правило, эти циклические проблемы с правами собственности возникают из-за того, что люди не думали должным образом о владении объектами. То, что объект должен указывать на что-то, не означает, что он должен иметь этот указатель. Обычно цитируемый пример - обратные указатели на деревьях, но родительский указатель на дереве может безопасно быть необработанным указателем без ущерба для безопасности.
Тим Сегин

4

Если вы используете 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 для многих целей это идеальный контейнер: произвольный доступ, безопасное исключение, предотвращение утечек памяти, низкие накладные расходы для перераспределения векторов (просто перетасовывает указатели за кулисы). Очень полезно для многих целей.


3

Хорошей практикой является документирование того, какие классы владеют указателями. Желательно, чтобы вы просто использовали обычные объекты и не указывали, когда бы ни могли.

Однако, когда вам нужно отслеживать ресурсы, передача указателей является единственной возможностью. Есть несколько случаев:

  • Вы получаете указатель откуда-то еще, но не управляете им: просто используйте обычный указатель и документируйте его, чтобы кодер не пытался удалить его.
  • Вы получаете указатель откуда-то еще и отслеживаете его: используйте scoped_ptr.
  • Вы получаете указатель откуда-то еще и отслеживаете его, но для его удаления требуется специальный метод: используйте shared_ptr с пользовательским методом удаления.
  • Вам нужен указатель в контейнере STL: он будет скопирован вокруг, поэтому вам нужен boost :: shared_ptr.
  • Многие классы имеют общий указатель, и неясно, кто его удалит: shared_ptr (случай выше на самом деле является частным случаем этой точки).
  • Вы сами создаете указатель, и он нужен только вам: если вы действительно не можете использовать обычный объект: scoped_ptr.
  • Вы создаете указатель и делитесь им с другими классами: shared_ptr.
  • Вы создаете указатель и передаете его: используйте обычный указатель и документируйте свой интерфейс, чтобы новый владелец знал, что он должен управлять ресурсом сам!

Я думаю, что это в значительной степени охватывает то, как я управляю своими ресурсами прямо сейчас. Стоимость памяти для указателя, такого как shared_ptr, обычно вдвое больше, чем для обычного указателя. Я не думаю, что эти издержки слишком велики, но если у вас мало ресурсов, вам следует подумать о разработке своей игры, чтобы уменьшить количество умных указателей. В других случаях я просто создаю хорошие принципы, такие как приведенные выше, и профилировщик скажет мне, где мне понадобится больше скорости.


1

Что касается конкретно указателей boost, я думаю, что их следует избегать, если их реализация не совсем то, что вам нужно. Они приходят по цене, которая больше, чем кто-либо первоначально ожидал. Они предоставляют интерфейс, который позволяет вам пропустить важные и важные части вашей памяти и управления ресурсами.

Когда дело доходит до разработки программного обеспечения, я думаю, что важно думать о ваших данных. Очень важно, как ваши данные представлены в памяти. Причина этого заключается в том, что скорость процессора увеличивается гораздо быстрее, чем время доступа к памяти. Это часто делает кэши памяти главным узким местом большинства современных компьютерных игр. Линейное выравнивание данных в памяти в соответствии с порядком доступа намного удобнее для кэша. Такие решения часто приводят к более чистым проектам, более простому коду и определенно коду, который легче отлаживать. Умные указатели легко приводят к частому динамическому распределению ресурсов в памяти, что приводит к их разбросу по всей памяти.

Это не преждевременная оптимизация, это здоровое решение, которое можно и нужно принимать как можно раньше. Это вопрос архитектурного понимания оборудования, на котором будет работать ваше программное обеспечение, и это важно.

Изменить: Есть несколько вещей, которые следует учитывать относительно производительности общих указателей:

  • Счетчик ссылок выделен в куче.
  • Если вы используете потоковую безопасность включена, подсчет ссылок осуществляется через операции с блокировкой.
  • Передача указателя по значению изменяет счетчик ссылок, что означает, что блокированные операции, вероятнее всего, используют произвольный доступ в памяти (блокировки + вероятное отсутствие кэша).

2
Вы потеряли меня из-за «избежать любой ценой». Затем вы продолжаете описывать тип оптимизации, который редко касается реальных игр. Большая часть разработки игр характеризуется проблемами разработки (задержки, ошибки, играбельность и т. Д.), А не недостатком производительности кеш-памяти процессора. Поэтому я категорически не согласен с идеей, что этот совет не является преждевременной оптимизацией.
kevin42

2
Я должен согласиться с ранним дизайном макета данных. Важно получить любую производительность от современной консоли / мобильного устройства, и это то, что никогда не следует упускать из виду.
Olly

1
Это проблема, которую я видел в одной из AAA-студий, над которой я работал. Вы также можете послушать главного архитектора Игр бессонницы Майка Актона. Я не говорю, что boost - плохая библиотека, она не просто хорошо подходит для высокопроизводительных игр.
Саймон

1
@ kevin42: когерентность кэша, вероятно, является сегодня основным источником низкоуровневых оптимизаций в разработке игр. @Simon: Большинство реализаций shared_ptr избегают блокировок на любой платформе, которая поддерживает сравнение и обмен, включая ПК с Linux и Windows, и я полагаю, включает Xbox.

1
@Joe Wreschnig: Это правда, промах кэша все еще наиболее вероятен, хотя и вызывает любую инициализацию общего указателя (копирование, создание из слабого указателя и т. Д.). Недостаток кэша L2 на современных компьютерах составляет 200 циклов, а на PPC (xbox360 / ps3) он выше. В интенсивной игре у вас может быть до 1000 игровых объектов, учитывая, что у каждого игрового объекта может быть довольно много ресурсов, и мы рассматриваем проблемы, в которых их масштабирование является главной проблемой. Это может вызвать проблемы в конце цикла разработки (когда вы попадете в большое количество игровых объектов).
Саймон

0

Я склонен использовать умные указатели везде. Я не уверен, является ли это абсолютно хорошей идеей, но я ленивый, и я не вижу никакого реального недостатка [за исключением того, что я хотел сделать некоторую арифметику указателя в стиле C]. Я использую boost :: shared_ptr, потому что я знаю, что могу скопировать его - если два объекта совместно используют изображение, то, если один из них умирает, другой не должен потерять изображение.

Недостатком этого является то, что если один объект удаляет то, на что он указывает и владеет, но что-то еще указывает на него, он не удаляется.


1
Я также использовал share_ptr почти везде, но сегодня я пытаюсь подумать, действительно ли мне нужно совместное владение каким-либо фрагментом данных. Если нет, то было бы разумно сделать эти данные не указателем на родительскую структуру данных. Я считаю, что четкое владение упрощает дизайн.
jmp97

0

Преимущества управления памятью и документация, предоставляемые хорошими умными указателями, означают, что я использую их регулярно. Однако, когда профилировщик подключается к сети и сообщает мне, что конкретное использование обходится мне дорого, я вернусь к более неолитическому управлению указателями.


0

Я старый, oldskool и счетчик циклов. В своей работе я использую сырые указатели и не динамические выделения во время выполнения (кроме самих пулов). Все объединено, и владение очень строго и никогда не может передаваться, если действительно необходимо, я пишу собственный распределитель небольших блоков. Я удостоверяюсь, что во время игры есть состояние, чтобы каждый пул мог очистить себя. Когда вещи становятся волосатыми, я заворачиваю предметы в ручки, чтобы переместить их, но я бы предпочел этого не делать. Контейнеры изготовлены на заказ и чрезвычайно голые. Я также не использую код повторно.
Хотя я бы никогда не стал спорить о достоинствах всех умных указателей, контейнеров, итераторов и так далее, я известен тем, что умею кодировать очень быстро (и достаточно надежно), хотя другим не рекомендуется прыгать в мой код по несколько очевидным причинам, как сердечные приступы и вечные кошмары).

На работе, конечно, все по-другому, если только я не занимаюсь прототипированием, которое я, к счастью, много делаю.


0

Почти ничего, хотя это, по общему признанию, странный ответ, и, вероятно, нигде не подходит для всех.

Но я обнаружил, что в моем личном случае гораздо полезнее хранить все экземпляры определенного типа в центральной последовательности с произвольным доступом (поточно-ориентированной) и вместо этого работать с 32-разрядными индексами (относительными адресами, т.е.). , а не абсолютные указатели.

Для начала:

  1. Это вдвое уменьшает требования к памяти для аналогового указателя на 64-битных платформах. До сих пор мне никогда не требовалось более ~ 4,29 миллиарда экземпляров определенного типа данных.
  2. Это гарантирует, что все экземпляры определенного типа, Tникогда не будут слишком разбросаны в памяти. Это имеет тенденцию уменьшать потери кэша для всех типов шаблонов доступа, даже обходя связанные структуры, такие как деревья, если узлы связаны друг с другом, используя индексы, а не указатели.
  3. Параллельные данные легко связать, используя дешевые параллельные массивы (или разреженные массивы) вместо деревьев или хеш-таблиц.
  4. Набор пересечений может быть найден в линейном времени или лучше, например, с использованием параллельного набора битов.
  5. Мы можем радикально отсортировать индексы и получить очень удобный кеш-шаблон последовательного доступа.
  6. Мы можем отслеживать, сколько экземпляров того или иного типа данных было выделено.
  7. Минимизирует количество мест, где приходится иметь дело с такими вещами, как безопасность исключений, если вы заботитесь о таких вещах.

Тем не менее, удобство является недостатком, а также безопасность типов. Мы не можем получить доступ к экземпляру , Tне имея доступа к как контейнер и индекс. А старый добрый старый int32_tнам ничего не говорит о том, к какому типу данных он относится, поэтому нет безопасности типов. Мы могли бы случайно попытаться получить доступ Barк индексу Foo. Чтобы смягчить вторую проблему, я часто делаю такие вещи:

struct FooIndex
{
    int32_t index;
};

Что кажется глупым, но возвращает мне безопасность типов, так что люди не могут случайно попытаться получить доступ Barчерез индекс Fooбез ошибки компилятора. Для удобства я просто принимаю небольшие неудобства.

Еще одна вещь, которая может быть большим неудобством для людей, заключается в том, что я не могу использовать полиморфизм на основе наследования в стиле ООП, поскольку для этого потребуется базовый указатель, который может указывать на все виды различных подтипов с различными размерами и требованиями выравнивания. Но я не очень часто использую наследование - предпочитаю подход ECS.

Что касается shared_ptr, я стараюсь не использовать это так много. Большую часть времени я не нахожу смысла делиться собственностью, и это может привести к логическим утечкам. Часто, по крайней мере на высоком уровне, одна вещь имеет тенденцию принадлежать одной вещи. Там, где мне часто shared_ptrприходилось заманчиво использовать, было продление срока службы объекта в местах, где на самом деле не так много внимания уделялось владению, как просто в локальной функции в потоке, чтобы убедиться, что объект не уничтожен до завершения потока используй это.

Чтобы решить эту проблему, вместо использования shared_ptrили GC или чего-то подобного, я часто отдаю предпочтение краткосрочным задачам, выполняющимся из пула потоков, и делаю так, чтобы, если этот поток запрашивал уничтожение объекта, фактическое уничтожение было отложено до безопасного время, когда система может гарантировать, что никакой поток не должен обращаться к указанному типу объекта.

Я все еще иногда заканчиваю тем, что использую пересчет, но рассматриваю это как стратегию последней инстанции. И есть несколько случаев, когда действительно имеет смысл разделять владение, например, внедрение постоянной структуры данных, и я считаю, что это имеет смысл сразу же достичь shared_ptr.

Так или иначе, я в основном использую индексы и редко использую как сырые, так и умные указатели. Мне нравятся индексы и виды дверей, которые они открывают, когда вы знаете, что ваши объекты хранятся непрерывно, а не разбросаны по пространству памяти.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.