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

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

Вы можете рассмотреть

  • собственность на объект
  • простота использования
  • политика копирования
  • накладные расходы
  • циклические ссылки
  • целевая платформа
  • использовать с контейнерами
48 голосов | спросил 2 revs, 2 users 100%
jmp97
1 Jam1000000amThu, 01 Jan 1970 03:00:00 +030070 1970, 03:00:00

10 ответов


32

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

  

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

     

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

     

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

ответил Anton 6 12017vEurope/Moscow11bEurope/MoscowMon, 06 Nov 2017 20:21:31 +0300 2017, 20:21:31
24

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

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

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

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

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

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

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

ответил Anton 6 12017vEurope/Moscow11bEurope/MoscowMon, 06 Nov 2017 20:21:31 +0300 2017, 20:21:31
10

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

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

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

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

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

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

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

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

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

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

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

ответил Anton 6 12017vEurope/Moscow11bEurope/MoscowMon, 06 Nov 2017 20:21:31 +0300 2017, 20:21:31
4

Если вы используете C ++ 0x, используйте std::unique_ptr<T>.

У него нет служебных накладных расходов, в отличие от std::shared_ptr<T>, который имеет служебные расходы на подсчет ссылок. Уникальный_ptr владеет своим указателем, и вы можете передать права собственности на семантику move C ++ 0x. Вы не можете копировать их - только перемещайте их.

Его также можно использовать в контейнерах, например. std::vector<std::unique_ptr<T>>, который является двоично-совместимым и идентичным по производительности до std::vector<T*>, но не будет утечка памяти, если вы удалите элементы или очистите вектор. Это также улучшает совместимость с алгоритмами STL, чем ptr_vector.

IMO для многих целей - это идеальный контейнер: произвольный доступ, исключение, предотвращение утечек памяти, низкие накладные расходы для перераспределения векторов (просто перетасовки вокруг указателей за кулисами). Очень полезно для многих целей.

ответил Anton 6 12017vEurope/Moscow11bEurope/MoscowMon, 06 Nov 2017 20:21:31 +0300 2017, 20:21:31
3

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

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

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

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

ответил Anton 6 12017vEurope/Moscow11bEurope/MoscowMon, 06 Nov 2017 20:21:31 +0300 2017, 20:21:31
1

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

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

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

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

  • Контрольный счетчик выделяется кучей.
  • Если вы используете защиту от потоков, подсчет ссылок выполняется посредством блокированных операций.
  • Передача указателя по значению изменяет счетчик ссылок, что означает блокированные операции, наиболее вероятно используя случайный доступ в памяти (блокирует + вероятность промаха в кеше).
ответил Anton 6 12017vEurope/Moscow11bEurope/MoscowMon, 06 Nov 2017 20:21:31 +0300 2017, 20:21:31
0

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

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

ответил Anton 6 12017vEurope/Moscow11bEurope/MoscowMon, 06 Nov 2017 20:21:31 +0300 2017, 20:21:31
0

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

ответил Anton 6 12017vEurope/Moscow11bEurope/MoscowMon, 06 Nov 2017 20:21:31 +0300 2017, 20:21:31
0

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

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

ответил Anton 6 12017vEurope/Moscow11bEurope/MoscowMon, 06 Nov 2017 20:21:31 +0300 2017, 20:21:31
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, было продление срока жизни объекта в местах, которые на самом деле не занимались владением так сильно, как только локальная функция в потоке, чтобы убедиться, что объект isn 't уничтожен до того, как поток закончит его использование.

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

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

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

ответил Anton 6 12017vEurope/Moscow11bEurope/MoscowMon, 06 Nov 2017 20:21:31 +0300 2017, 20:21:31

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132