Коли використовувати shared_ptr і коли використовувати необроблені вказівники?


78
class B;

class A
{
public:
    A ()
        : m_b(new B())
    {
    }

    shared_ptr<B> GimmeB ()
    {
        return m_b;
    }

private:
    shared_ptr<B> m_b;
};

Скажімо, B - це клас, який семантично не повинен існувати поза життям A, тобто немає абсолютно жодного сенсу для B існувати сам по собі. Потрібно GimmeBповернути a shared_ptr<B>чи a B*?

Загалом, чи є гарною практикою повністю уникати використання необроблених покажчиків у коді С ++ замість розумних вказівників?

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


Я припускаю, що неможливо повернути посилання замість покажчиків?
Ricibob,

Це добре ... для обговорення припустимо, що покажчик повинен бути повернутий.
TripShock

Крім того, якщо я хочу задати пов’язане / дуже подібне запитання, чи прийнятно редагувати це та додати його сюди, чи я повинен поставити його як інше питання?
TripShock

Відповіді:


76

Я думаю, ваш аналіз цілком коректний. У цій ситуації я також поверну оголений B*або навіть [const] B&якщо об'єкт гарантовано ніколи не буде нульовим.

Витративши деякий час на вивчення розумних покажчиків, я дійшов деяких рекомендацій, які вказують мені, що робити в багатьох випадках:

  • Якщо ви повертаєте об'єкт, тривалістю життя якого повинен керувати абонент, поверніться std::unique_ptr. Той, хто телефонує, може призначити його, std::shared_ptrякщо хоче.
  • Повернення std::shared_ptrнасправді є досить рідкісним, і коли це має сенс, це, як правило, очевидно: ви вказуєте абоненту, що це продовжить термін дії об'єкта, на який спрямовано вказівник, понад термін життя об'єкта, який спочатку підтримував ресурс. Повернення спільних покажчиків із заводів не є винятком: ви повинні це зробити, наприклад. коли ви використовуєте std::enable_shared_from_this.
  • Вам дуже рідко потрібно std::weak_ptr, за винятком випадків, коли ви хочете зрозуміти lockметод. Це має деякі варіанти використання, але вони рідкісні. У вашому прикладі, якби час життя Aоб’єкта не був детермінованим з точки зору абонента, це було б що розглянути.
  • Якщо ви повертаєте посилання на існуючий об'єкт, життя якого абонент не може контролювати, тоді поверніть голий покажчик або посилання. Роблячи це, ви повідомляєте абоненту, що об'єкт існує та що вона не повинна піклуватися про його життя. Вам слід повернути посилання, якщо ви не використовуєте nullptrзначення.

5
" продовжить життя " " дуже рідко потребуєstd::weak_ptr " Слухай! чути!
curiousguy

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

@ShitalShah: Якщо абонент контролює тривалість життя, вкажіть це явно з std::unique_ptr(або, можливо, я не зовсім зрозумів, що ви мали на увазі).
Олександр К.

1
чи слід повертати shared_ptr із шаблону заводського дизайну?
Stav Alfi

29

Питання "коли слід використовувати shared_ptrі коли слід використовувати сирі покажчики?" має дуже просту відповідь:

  • Використовуйте необроблені вказівники, коли ви не хочете, щоб будь-яке право власності було прикріплене до вказівника. Цю роботу також часто можна виконати з посиланнями. Сировинні вказівники також можуть бути використані в деяких кодах низького рівня (наприклад, для реалізації розумних вказівників або реалізації контейнерів).
  • Використовуйте unique_ptrабо scope_ptrколи ви хочете мати унікальне право власності на об’єкт. Це найкорисніший варіант, і його слід використовувати в більшості випадків. Унікальне право власності також може бути виражене простим створенням об'єкта безпосередньо, а не використанням вказівника (це навіть краще, ніж використання a unique_ptr, якщо це можливо зробити).
  • Використовуйте shared_ptrабо intrusive_ptrколи ви хочете спільне володіння вказівником. Це може заплутати та неефективно, і часто не є вдалим варіантом. Спільне володіння може бути корисним у деяких складних проектах, але його слід взагалі уникати, оскільки воно веде до коду, який важко зрозуміти.

shared_ptrs виконують зовсім інше завдання, ніж вихідні вказівники, і ні shared_ptrs, ні необроблені вказівники не є найкращим варіантом для більшості коду.


11

Нижче наведено хороше емпіричне правило:

  • Коли немає передачі або спільного володіння, посилання або прості вказівки досить хороші. (Звичайні вказівники є більш гнучкими, ніж посилання.)
  • Тоді коли є передача права власності, але немає спільної власності std::unique_ptr<>, це хороший вибір. Часто справа з заводськими функціями.
  • Коли є спільне право власності, це хороший варіант використання для std::shared_ptr<>або boost::intrusive_ptr<>.

Найкраще уникати спільного володіння, частково тому, що вони є найдорожчими з точки зору копіювання і std::shared_ptr<>займають вдвічі більше місця для зберігання звичайного вказівника, але, найголовніше, тому, що вони сприяють поганому дизайну, де немає чітких власників, які, в свою чергу, призводить до волосинки предметів, які неможливо знищити, оскільки вони тримають спільні вказівники один до одного.

Найкращий дизайн - це чітке володіння та ієрархічність, так що в ідеалі взагалі не потрібні розумні вказівники. Наприклад, якщо є фабрика, яка створює унікальні об'єкти або повертає існуючі, має сенс фабриці володіти об'єктами, які вона створює, і просто зберігати їх за значенням в асоціативному контейнері (наприклад std::unordered_map), щоб він міг повернути вказівники або посилання на своїх користувачів. Ця фабрика повинна мати час життя, який починається до першого користувача та закінчується після останнього користувача (ієрархічна властивість), щоб користувачі не могли мати вказівник на вже зруйнований об'єкт.


7
Зверніть увагу, що std :: auto_ptr застарілий із C ++ 11 і буде вилучений із C ++ 17.
Sydius

" тому що вони тримають спільні вказівники один до одного " Тільки якщо у вас є серйозна помилка в дизайні
curiousguy

6

Якщо ви не хочете, щоб викликана особа GimmeB () могла продовжити термін життя вказівника, зберігаючи копію ptr після того, як екземпляр A помер, тоді вам точно не слід повертати shared_ptr.

Якщо викликана особа не повинна тримати повернутий покажчик протягом тривалих періодів часу, тобто немає ризику, що екземпляр життя A закінчується до покажчика, тоді сирий вказівник буде кращим. Але навіть кращий вибір - просто використовувати посилання, якщо немає вагомих причин використовувати фактичний необроблений вказівник.

І, нарешті, у випадку, якщо повернутий покажчик може існувати після закінчення терміну дії екземпляра A, але ви не хочете, щоб сам вказівник продовжував життя B, тоді ви можете повернути слабкий_ptr, який ви можете використовувати для тестування чи все ще існує.

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


3

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

У вашому конкретному випадку: чому б не повернути посилання?

Вказівник припускає, що дані можуть бути нульовими, однак тут Bу вашому завжди буде а A, отже, він ніколи не буде нульовим. Посилання стверджує таку поведінку.

Зважаючи на це, я бачив людей, які виступають за використання shared_ptrнавіть у середовищах, що не використовуються спільно, і weak_ptrпроводять ручки з ідеєю "захистити" програму та уникнути застарілих покажчиків. На жаль, оскільки ви можете відновити a shared_ptrз weak_ptr(і це єдиний спосіб насправді маніпулювати даними), це все одно є спільним правом власності, навіть якщо воно не передбачалося.

Примітка. Існує незначна помилка. shared_ptrКопія Aфайлу поділятиметься Bоригіналом за замовчуванням, якщо ви явно не напишете конструктор копіювання та оператор присвоєння копії. І звичайно, ви б не використовували сирий вказівник Aдля утримання B, правда :)?


Звичайно, інше питання полягає в тому, чи насправді потрібно це робити. Одним із принципів хорошого дизайну є інкапсуляція . Для досягнення інкапсуляції:

Ви не повинні повертати ручки своїм внутрішнім органам (див. Закон Деметри ).

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


Ваша записка - це тонкий жарт? Якщо так, це занадто тонко для мене, якщо ні, то це та сама помилка, що і з необробленим покажчиком (за винятком подвійного видалення або витоку пам'яті з необробленими вказівниками)
stefaanv

@stefaanv: зауважте, що у питанні про OP член-член не обов'язково динамічно розподіляється. Його слід динамічно розподіляти, лише якщо використовується shared_ptrрішення, однак при поверненні посилання він цілком може бути звичайним атрибутом, і звичайні атрибути не копіюються поверхнево.
Matthieu M.

@Matthieu, добре, тоді мені примітка не дуже зрозуміла. (на прикладі ОП чітко видно динамічне розподіл, і його запитання
стосувалось

@stefaanv: OP показує динамічний вказівник, тому обов’язково динамічне розподіл і питання, я відчуваю, я лише про те, з чого слід повернутися gimmeB(хоча відповідь може вплинути на внутрішнє уявлення). Спробую уточнити нотатку.
Matthieu M.

BTW: +1 для вашого додавання: питання 1: чи дійсно я повинен викривати внутрішні елементи?
stefaanv

2

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

Якщо питання власності є проблемою, використовуйте розумний вказівник. Якщо ні, я б використав посилання, якщо це можливо.


2
  1. Ви виділяєте B при спорудженні A.
  2. Ви кажете, що Б не повинен зберігатись назовні як усе життя.
    Обидва вони вказують на те, що B є членом A і щойно повертає посилальний доступ. Ви надмірно інженеруєте це?

2

Я виявив, що основні настанови C ++ дають кілька дуже корисних підказок щодо цього питання:

Використання необробленого вказівника (T *) або розумнішого вказівника залежить від того, кому належить об'єкт (відповідальність за звільнення пам'яті obj).

власний:

smart pointer, owner<T*>

не власний:

T*, T&, span<>

owner <>, span <> визначено в бібліотеці Microsoft GSL

ось основні правила:

1) ніколи не використовуйте сирий вказівник (або не власний тип) для передачі права власності

2) розумний вказівник слід використовувати лише тоді, коли призначена семантика власності

3) T * або власник позначає окремий об'єкт (лише)

4) використовувати для масиву vector / array / span

5) На мій погляд, shared_ptr зазвичай використовується, коли ви не знаєте, хто випустить obj, наприклад, один obj використовується багатопоточним


1

Рекомендується уникати використання сирих покажчиків, але ви не можете просто замінити все на shared_ptr . У цьому прикладі користувачі вашого класу вважатимуть, що нормально продовжувати термін служби B понад термін дії A, і можуть вирішити деякий час утримувати повернутий об’єкт B з власних причин. Ви повинні повернути a weak_ptr, або, якщо B абсолютно не може існувати, коли A знищено, посилання на B або просто сирий вказівник.


Повернення a weak_ptrнасправді не вирішує проблему "користувачі вашого класу вважатимуть, що це нормально продовжити життя B", оскільки вони можуть заблокувати слабкий_ptr у точці, де A зруйнований, і тому вони все одно можуть продовжити життя B. Однак це може змусити їх задуматись, і звичайно, якщо трапиться, що вони не заблокують при руйнуванні A, тоді B може піти теж.
Steve Jessop

0

Коли ви говорите: "Скажімо, B - це клас, який семантично не повинен існувати поза життям A"

Це говорить мені, що B логічно не повинен існувати без A, але як щодо фізично існуючих? Якщо ви можете бути впевнені, що ніхто не спробує використовувати * B після A dtors, ніж, можливо, сирий вказівник буде нормальним. В іншому випадку може підійти розумніший вказівник.

Коли клієнти мають прямий вказівник на A, ви повинні довіряти, що вони оброблять це належним чином; не намагайтеся dtoring це тощо

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