Як повернути розумні вказівники (shared_ptr), за посиланням чи за значенням?


94

Скажімо, у мене є клас із методом, який повертає a shared_ptr.

Які можливі переваги та недоліки повернення його за посиланням чи за вартістю?

Дві можливі підказки:

  • Раннє знищення об’єкта. Якщо я повертаю посилання shared_ptrby (const), лічильник посилань не збільшується, тому я ризикую видалити об'єкт, коли він виходить за межі області дії в іншому контексті (наприклад, інший потік). Це правильно? Що робити, якщо середовище однопоточне, чи може статися і така ситуація?
  • Вартість. Перехідне значення, безумовно, не є безкоштовним. Чи варто уникати цього, коли це можливо?

Дякую всім.

Відповіді:


114

Поверніть розумні вказівники за значенням.

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

Проблема витрат в наш час спірна завдяки оптимізації поверненого значення (RVO), тому у вас не виникне послідовність збільшення-збільшення-зменшення або щось подібне у сучасних компіляторах. Тож найкращий спосіб повернути a shared_ptr- це просто повернути за значенням:

shared_ptr<T> Foo()
{
    return shared_ptr<T>(/* acquire something */);
};

Це абсолютно очевидна можливість RVO для сучасних компіляторів C ++. Я точно знаю, що компілятори Visual C ++ реалізують RVO, навіть коли всі оптимізації вимкнені. А з семантикою переміщення C ++ 11 це занепокоєння є ще менш актуальним. (Але єдиний спосіб бути впевненим - це профіль та експерименти.)

Якщо ви все ще не впевнені, у Дейва Абрахамса є стаття, яка наводить аргументи для повернення за значенням. Я відтворюю тут фрагмент; Настійно рекомендую прочитати всю статтю:

Будьте чесними: як почувається такий код?

std::vector<std::string> get_names();
...
std::vector<std::string> const names = get_names();

Чесно кажучи, хоч я і мав би це знати краще, це мене нервує. В принципі, коли get_names() повертається, ми повинні копіюйте vectorз stringс. Потім нам потрібно скопіювати його ще раз, коли ми ініціалізуємо names, і нам потрібно знищити першу копію. Якщо stringу векторі є N s, кожна копія може вимагати виділення пам’яті N + 1 та цілий ряд недружнього доступу до кешу даних>, оскільки копіюється вміст рядка.

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

get_names(std::vector<std::string>& out_param );
...
std::vector<std::string> names;
get_names( names );

На жаль, такий підхід далеко не ідеальний.

  • Код виріс на 150%
  • Нам довелося відмовитись, constтому що ми мутуємо імена.
  • Як нам люблять нагадувати функціональні програмісти, мутація робить код більш складним для роздумів, підриваючи референційну прозорість та еквівалентні міркування.
  • У нас більше немає суворої семантики значень для імен.

Але чи насправді потрібно зіпсувати наш код таким чином, щоб отримати ефективність? На щастя, відповідь виявляється ні (і особливо ні, якщо ви використовуєте C ++ 0x).


Я не знаю, що я б сказав, що RVO робить питання спірним, оскільки повернення через посилання рішуче робить RVO неможливим.
Едвард Стрендж,

@CrazyEddie: Правда, це одна з причин, чому я рекомендую OP повернути за значенням.
In silico

Чи правило RVO, дозволене стандартом, має перевагу над правилами щодо синхронізації / взаємозв'язку "до подій", гарантованих стандартом?
edA-qa mort-ora-y

1
@ edA-qa mort-ora-y: RVO явно дозволено, навіть якщо він має побічні ефекти. Наприклад, якщо у вас є cout << "Hello World!";оператор у конструкторі за замовчуванням та копію, ви не побачите двох Hello World!s, коли діє RVO. Однак це не повинно бути проблемою для правильно розроблених розумних покажчиків, навіть без синхронізації.
In silico

23

Щодо будь-якого розумного вказівника (а не просто shared_ptr), я не думаю, що коли-небудь прийнятно повертати посилання на нього, і я б дуже вагався передавати їх за допомогою посилання або необробленого вказівника. Чому? Оскільки ви не можете бути впевнені, що згодом він не буде скопійований неглибоко через посилання. Ваш перший пункт визначає причину, через яку це повинно викликати занепокоєння. Це може статися навіть в однопоточному середовищі. Вам не потрібен одночасний доступ до даних, щоб додати у ваші програми семантику поганих копій. Ви насправді не контролюєте, що ваші користувачі роблять із покажчиком, коли його передаєте, тому не заохочуйте зловживання, даючи вашим користувачам API достатньо мотузки, щоб повіситись.

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

Редагувати, через 3 роки: з появою більш сучасних функцій в C ++, я б підкоригував свою відповідь, щоб прийняти більше випадків, коли ви просто написали лямбду, яка ніколи не живе поза сферою дії виклику і не є скопійовано десь ще. Тут, якщо ви хочете заощадити мінімальні накладні витрати на копіювання спільного вказівника, це було б справедливо і безпечно. Чому? Оскільки ви можете гарантувати, що посилання ніколи не буде використано неправильно.

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