C ++: Смарт-покажчики, Сирі вказівники, Без покажчиків? [зачинено]


48

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

Ви можете розглянути

  • право власності на об'єкт
  • простота використання
  • копіювати політику
  • накладні
  • циклічні посилання
  • цільова платформа
  • використання з контейнерами

Відповіді:


32

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

Якщо вам дійсно потрібна семантика вказівника, scoped_ptr - це чудово. Ви повинні використовувати std :: tr1 :: shared_ptr лише в дуже специфічних умовах, наприклад, коли об'єкти повинні містити контейнери STL. Ніколи не слід використовувати auto_ptr. [...]

Взагалі ми вважаємо за краще, щоб ми розробляли код із чітким правом власності на об'єкт. Найясніша власність на об'єкт отримується шляхом використання об'єкта безпосередньо як поля чи локальної змінної, без використання вказівників взагалі. [..]

Хоча вони не рекомендуються, посилання, що підраховуються, іноді є найпростішим та найелегантнішим способом вирішення проблеми.


14
Сьогодні ви можете використовувати std :: unique_ptr замість scoped_ptr.
Клаїм

24

Я також слідую за думкою "сильної власності". Мені подобається чітко окреслити, що "цей клас є власником цього члена", коли це доречно.

Я рідко використовую shared_ptr. Якщо я це роблю, я ліберально використовую, weak_ptrколи можу, тому можу ставитись до цього, як до ручки до об'єкта, а не збільшувати кількість відліку.

Я використовую scoped_ptrвсюди. Це показує очевидну власність. Єдина причина, що я не просто створюю такі об'єкти, як член, це те, що ви можете переслати їх, якщо вони знаходяться в scoped_ptr.

Якщо мені потрібен список об’єктів, я використовую ptr_vector. Це ефективніше і має менше побічних ефектів, ніж використання vector<shared_ptr>. Я думаю, ви, можливо, не зможете переслати декларувати тип у ptr_vector (минув певний час), але семантика цього робить його вартим того, на мою думку. Якщо ви видалите об'єкт зі списку, він видаляється автоматично. Це також показує очевидну власність.

Якщо мені потрібне посилання на щось, я намагаюся зробити це посиланням замість оголеного вказівника. Іноді це не практично (тобто коли-небудь потрібна довідка після побудови об'єкта). Так чи інакше, посилання очевидно показують, що ви не є власником об'єкта, і якщо ви дотримуєтесь спільної семантики вказівників скрізь, то голі покажчики, як правило, не викликають додаткової плутанини (особливо якщо ви дотримуєтесь правила "не видаляється вручну") .

За допомогою цього методу в одній грі iPhone, над якою я працював, вдалося здійснити лише один deleteдзвінок, і це було на мосту Obj-C to 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 Container або збереження інтелектуальних покажчиків спільного володіння у Containers. У критичному виконанні коду ви можете уникати обох на користь нав'язливих контейнерів, таких як Boost.Intrusive.

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


3
Поводження з винятками на консолях може бути трохи хитким - зокрема XDK є на зразок ворожої винятку.
Crashworks

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

Я не погоджуюся лише трохи, конкретно щодо ГК. Здебільшого циклічні посилання не є проблемою для відлічених схем. Як правило, ці циклічні проблеми власності виникають через те, що люди не задумуються належним чином щодо права власності на об’єкти. Тільки тому, що об'єкт повинен вказувати на щось, не означає, що він повинен володіти цим вказівником. Найпоширеніший приклад - зворотні покажчики на деревах, але батьківський вказівник на дереві може бути безпечним покажчиком без шкоди для безпеки.
Тім Сегейн

4

Якщо ви використовуєте C ++ 0x, використовуйте std::unique_ptr<T>.

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

Він також може бути використаний у контейнерах, наприклад std::vector<std::unique_ptr<T>>, який є бінарним сумісним та однаковим по продуктивності std::vector<T*>, але не просочується пам'яттю, якщо ви стираєте елементи чи очищаєте вектор. Це також має кращу сумісність із алгоритмами STL, ніж ptr_vector.

ІМО для багатьох цілей - це ідеальний контейнер: випадковий доступ, безпечний виняток, запобігає витоку пам’яті, низькі накладні витрати для перерозподілу векторів (просто перетасовується навколо вказівників за кадром). Дуже корисно для багатьох цілей.


3

Доброю практикою є документування того, яким класам належать які покажчики. Переважно, ви просто використовуєте звичайні об'єкти, а не вказівники, коли зможете.

Однак, коли потрібно стежити за ресурсами, проходження покажчиків - єдиний варіант. Є деякі випадки:

  • Ви отримуєте вказівник з іншого місця, але не керуєте ним: просто використовуйте звичайний вказівник і документуйте його так, щоб жоден кодер не намагався його видалити.
  • Ви отримуєте вказівник десь з іншого місця, і ви стежите за ним: використовуйте scoped_ptr.
  • Ви отримуєте вказівник з іншого місця, і ви стежите за ним, але для його видалення потрібен спеціальний метод: використовуйте shared_ptr зі спеціальним методом видалення.
  • Вам потрібен вказівник у контейнері STL: він буде скопійований навколо, тому вам потрібно boost :: shared_ptr.
  • Багато класів поділяють вказівник, і незрозуміло, хто його видалить: shared_ptr (випадок вище насправді є особливим випадком цієї точки).
  • Ви створюєте вказівник самостійно, і тільки він вам потрібен: якщо ви дійсно не можете використовувати звичайний об’єкт: scoped_ptr.
  • Ви створите покажчик і поділитесь ним з іншими класами: shared_ptr.
  • Ви створюєте покажчик і передаєте його: використовуйте звичайний вказівник і документуйте свій інтерфейс, щоб новий власник знав, що він повинен сам керувати ресурсом!

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


1

Що стосується конкретних покажчиків підвищення, я думаю, що їх слід уникати, якщо їх реалізація не є точно тим, що вам потрібно. Вони йдуть ціною, більшою, ніж спочатку очікували б. Вони надають інтерфейс, який дозволяє пропускати життєво важливі та важливі частини вашої пам’яті та управління ресурсами.

Що стосується будь-якої розробки програмного забезпечення, я думаю, що важливо продумати свої дані. Дуже важливо, як ваші дані представлені в пам'яті. Причиною цього є те, що швидкість процесора зростала набагато більшою швидкістю, ніж час доступу до пам'яті. Це часто робить кеші пам'яті головними вузькими місцями більшості сучасних комп'ютерних ігор. Якщо ваші дані лінійно вирівняні в пам'яті відповідно до порядку доступу, це набагато привітніше кешу. Такі рішення часто призводять до більш чистих конструкцій, більш простого коду та, безумовно, коду, який простіше налагодити. Розумні покажчики легко призводять до частого динамічного розподілу пам'яті ресурсів, це призводить до їх розсіювання по всій пам'яті.

Це не передчасна оптимізація, це здорове рішення, яке можна і потрібно приймати якомога раніше. Це питання архітектурного розуміння обладнання, на якому буде працювати ваше програмне забезпечення, і це важливо.

Редагувати: Є кілька моментів, які слід врахувати щодо продуктивності загальних покажчиків:

  • Контрольний лічильник виділяється купи.
  • Якщо ви використовуєте функцію безпеки потоку, підрахунок посилань здійснюється через заблоковані операції.
  • Передача покажчика за значенням змінює кількість посилань, що означає блоковані операції, найімовірніше, використання випадкового доступу в пам'яті (блокування + ймовірний пропуск кешу).

2
Ви втратили мене за "уникнути будь-якої ціни". Потім ви описуєте тип оптимізації, який рідко стосується реальних ігор. Більшість ігор характеризуються проблемами розвитку (затримки, помилки, відтворюваність тощо), а не відсутністю продуктивності кешу процесора. Тому я категорично не згоден з думкою, що ця порада не є передчасною оптимізацією.
kevin42

2
Я повинен погодитися з ранньою розробкою макета даних. Важливо отримати будь-яку продуктивність із сучасної консолі / мобільного пристрою, і це те, що ніколи не слід переглядати.
Оллі

1
Це питання, яке я бачив в одній із студій AAA, над якою працював. Ви також можете слухати головного архітектора в Іграх про безсоння Майка Ектона. Я не кажу, що бустер - це погана бібліотека, вона не просто підходить для ігор з високою ефективністю.
Саймон

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

1
@Joe Wreschnig: Це правда, кеш-міс все ще є, хоча викликає будь-яку ініціалізацію загального вказівника (копіювати, створювати зі слабкого вказівника тощо). Кеш-пропуск L2 у сучасних ПК становить приблизно 200 циклів, а на КПП (xbox360 / ps3) він вищий. У інтенсивній грі у вас може бути до 1000 ігрових об’єктів, враховуючи, що кожен ігровий об’єкт може мати досить багато ресурсів, ми розглядаємо проблеми, де їх масштабування є головним питанням. Це, ймовірно, спричинить проблеми в кінці циклу розробки (коли ви потрапите на велику кількість ігрових об'єктів).
Саймон

0

Я схильний використовувати розумні покажчики скрізь. Я не впевнений, що це абсолютно гарна ідея, але я лінивий, і не можу побачити реального недоліку [за винятком випадків, коли я хотів зробити деяку арифметику вказівника у стилі С]. Я використовую 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без помилки компілятора. Для зручності я просто приймаю невеликі незручності.

Інша річ, яка може бути великою незручністю для людей, - це те, що я не можу використовувати поліморфізм, заснований на спадщині в стилі OOP, оскільки це вимагатиме базового вказівника, який може вказувати на всі види різних підтипів з різними вимогами до розміру та вирівнювання. Але я не користуюся спадщиною в наші дні - віддаю перевагу підходу ECS.

Що стосується shared_ptr, я намагаюся не користуватися ним так сильно. Більшу частину часу я не вважаю, що має сенс розділяти право власності, і це робити випадково може призвести до логічних витоків. Часто хоча б на високому рівні одна річ, як правило, належить до однієї речі. Де я часто вважаю заманливим використовувати shared_ptrпродовження терміну експлуатації об’єкта в місцях, які не так сильно займаються правом власності, як просто локальна функція в потоці, щоб переконатися, що об'єкт не знищений до завершення потоку. використовуючи його.

Щоб вирішити цю проблему, замість того, щоб використовувати shared_ptrабо GC чи щось подібне, я часто віддаю перевагу короткочасним завданням, що працюють із пулу потоків, і роблю це, якщо цей потік вимагає знищити об'єкт, що фактичне знищення переноситься на безпечний час, коли система може гарантувати, що жоден потік не потребує доступу до вказаного типу об’єкта.

Я все ще іноді в кінцевому підсумку використовую перерахунок, але трактую це як стратегію в крайньому випадку. І є кілька випадків, коли справді має сенс розділити право власності, як, наприклад, реалізація стійкої структури даних, і там я вважаю, що має сенс звернутися до них shared_ptrвідразу.

Отже, я в основному використовую індекси, і в основному економно використовую як сирі, так і розумні покажчики. Мені подобаються показники та види дверей, які вони відкриваються, коли ви знаєте, що ваші предмети зберігаються постійно, а не розкидані по пам’яті.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.