Чи слід передавати shared_ptr за посиланням або за значенням?


270

Коли функція приймає shared_ptr(від boost або C ++ 11 STL), ви передаєте її:

  • за посиланням const: void foo(const shared_ptr<T>& p)

  • або за значенням void foo(shared_ptr<T> p):?

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

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


14
Проблема в тому, що вони не є рівнозначними. Довідкова версія кричить "Я збираюся псевдонім деякі shared_ptr, і я можу змінити його, якщо захочу", в той час як версія значення говорить "Я збираюсь скопіювати ваш shared_ptr, тому, хоча я можу змінити його, ви ніколи не дізнаєтесь. ) Параметр const-референс - це справжнє рішення, яке говорить: "Я збираюся псевдоніми деякі shared_ptr, і я обіцяю не змінювати його" (що надзвичайно схоже на семантику за значенням!)
GManNickG

2
Гей , я був би зацікавлений в ваших хлопців думки про повернення до shared_ptrчлену класу. Ви робите це const-refs?
Йоханнес Шауб - ліб

Третя можливість полягає в тому, щоб використовувати std :: move () з C ++ 0x, це
поміняє обмін спільним_ptr

@Johannes: Я б повернув його через const-посилання лише для того, щоб уникнути копіювання / повторного підрахунку. Потім знову повертаю всіх членів за допомогою const-reference, якщо вони не примітивні.
GManNickG

Відповіді:


229

Це питання обговорювали і відповіли Скотт, Андрій та Герб під час сеансу " Запитуйте все" на C ++ та після 2011 року . Дивіться з 4:34 на shared_ptrпродуктивність та правильність .

Коротше, немає підстав переходити за значенням, якщо мета не полягає у розподілі права власності на об'єкт (наприклад, між різними структурами даних або між різними потоками).

Якщо ви не можете перемістити-оптимізувати це, як пояснив Скотт Майєрс у відеозверненому відео, але це стосується фактичної версії C ++, яку ви можете використовувати.

Основне оновлення цієї дискусії відбулося під час інтерактивної панелі конференції GoingNative 2012 : Запитайте нас про все! що варто переглянути, особливо з 22:50 .


5
але, як показано тут, дешевше передати значення: stackoverflow.com/a/12002668/128384 не варто також враховувати це (принаймні, для аргументів конструктора тощо, коли спільний_ptr буде внесений до складу клас)?
stijn

2
@stijn Так і ні. Питання та відповіді, які ви вказуєте, є неповними, якщо не уточнює версію стандарту C ++, на яку він посилається. Дуже легко поширювати загальні правила ніколи та завжди, які просто вводять в оману. Якщо тільки читачі не знаходять часу, щоб ознайомитись зі статтею та посиланнями Девіда Абрахамса або взяти до уваги дату публікації та чинний стандарт C ++. Отже, обидві відповіді, моя і та, яку ви вказали, є правильною, враховуючи час публікації.
mloskot

1
" якщо немає багатопотокових " немає, MT не є особливим чином.
curiousguy

3
Я дуже пізно на вечірку, але моя причина хочу передати shared_ptr за значенням в тому, що це робить код коротшим і красивішим. Серйозно. Value*короткий і читабельний, але поганий, тому зараз мій код переповнений const shared_ptr<Value>&і він значно менш читабельний і просто ... менш охайний. Те , що раніше void Function(Value* v1, Value* v2, Value* v3)в даний час void Function(const shared_ptr<Value>& v1, const shared_ptr<Value>& v2, const shared_ptr<Value>& v3), і люди все в порядку з цим?
Олексій

7
@Alex Загальна практика - це створення псевдонімів (typedefs) відразу після класу. Для вашого прикладу: class Value {...}; using ValuePtr = std::shared_ptr<Value>;тоді ваша функція стає простішою: void Function(const ValuePtr& v1, const ValuePtr& v2, const ValuePtr& v3)і ви отримуєте максимальну продуктивність. Ось чому ви використовуєте C ++, чи не так? :)
4LegsDrivenCat

92

Ось Херб Саттер

Керівництво: Не передайте інтелектуальний вказівник як параметр функції, якщо ви не хочете використовувати або маніпулювати самим інтелектуальним вказівником, наприклад, для спільного використання або передачі права власності.

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

Керівництво: Використовуйте параметр non-const shared_ptr & only лише для зміни shared_ptr. Використовуйте const shared_ptr & як параметр лише в тому випадку, якщо ви не впевнені, чи приймете ви копію та надаєте право власності; в іншому випадку використовуйте натомість віджет * (або, якщо не зведений до нуля, віджет &).


3
Дякуємо за посилання на Саттер. Це відмінна стаття. Я не погоджуюся з ним у віджеті *, віддаючи перевагу додатковому <віджету &>, якщо доступний C ++ 14. віджет * занадто неоднозначний зі старого коду.
однойменний

3
+1 за включення віджета * та віджета & як можливості. Просто для розробки, передача віджета * або віджета &, мабуть, найкращий варіант, коли функція не вивчає / не змінює сам об’єкт вказівника. Інтерфейс є більш загальним, оскільки він не вимагає конкретного типу вказівника, а питання про продуктивність контрольного числа посилання_ptr ухиляється.
tgnottingham

4
Я думаю, що це має бути прийнятою відповіддю сьогодні, через друге керівництво. Це явно визнає недійсним прийняту відповідь, що говорить: немає причин проходити повз значення.
mbrt

62

Особисто я б використав constпосилання. Немає необхідності збільшувати кількість посилань, щоб знову її зменшити заради виклику функції.


1
Я не визнав голосування вашої відповіді, але перш ніж це буде питанням уподобань, існують плюси і мінуси для кожної з двох можливостей для розгляду. І було б добре знати і обговорювати тези плюси і мінуси. Після цього кожен може прийняти рішення сам.
Danvil

@Danvil: з огляду на те, як shared_ptrпрацює, єдиний можливий мінус, який не можна пройти через посилання, - це невелика втрата продуктивності. Тут є дві причини. а) функція псевдонімування вказівника означає, що копіюються дані, плюс дані лічильника (можливо, 2 для слабких коефіцієнтів), тому копіювати дані в раунд трохи дорожче. б) підрахунок атомних посилань трохи повільніше, ніж звичайний старий приріст / декрементний код, але він необхідний для безпечного потоку. Крім того, два методи однакові для більшості намірів і цілей.
Еван Теран

37

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


4
Downvote представляє свою думку без будь-яких цифр, щоб підтвердити її.
kwesolowski

22

Я побіг код , наведений нижче, один раз fooвзявши shared_ptrна const&і знову fooвзявши shared_ptrза значенням.

void foo(const std::shared_ptr<int>& p)
{
    static int x = 0;
    *p = ++x;
}

int main()
{
    auto p = std::make_shared<int>();
    auto start = clock();
    for (int i = 0; i < 10000000; ++i)
    {
        foo(p);
    }    
    std::cout << "Took " << clock() - start << " ms" << std::endl;
}

Використовуючи версію VS2015, x86 версії, на моєму процесорі Intel Core 2 quad (2,4 ГГц)

const shared_ptr&     - 10ms  
shared_ptr            - 281ms 

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


1
Чи можете ви сказати, які налаштування компілятора, платформи та оптимізації ви використовували?
Карлтон

Я використовував збірку налагодження vs2015, оновив відповідь, щоб використовувати збірку випуску зараз.
tcb

1
Мені цікаво, якщо після ввімкнення оптимізації ви отримаєте однакові результати з обома
Елліот Вудс

2
Оптимізація не дуже допомагає. проблема полягає в заблокуванні суперечок щодо відліку посилання на копію.
Алекс

1
Це не сенс. Така foo()функція не повинна навіть сприймати загальний покажчик в першу чергу, оскільки вона не використовує цей об'єкт: вона повинна приймати a int&і do p = ++x;, дзвонивши foo(*p);з main(). Функція приймає об'єкт інтелектуального вказівника, коли йому потрібно щось зробити з ним, і більшість часу те, що вам потрібно зробити, - це перемістити його ( std::move()) в інше місце, тому параметр по вартості не має витрат.
eepp

15

Оскільки C ++ 11, ви повинні приймати його за значенням над const & частіше, ніж ви можете подумати.

Якщо ви приймаєте std :: shared_ptr (а не основний тип T), ви робите це, тому що хочете щось з ним зробити.

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

Звичайно, якщо абоненту потрібен його shared_ptr довше (таким чином, не може std :: перемістити його), і ви не хочете створювати просту копію у функції (скажіть, що вам потрібен слабкий покажчик, або ви лише іноді хочете скопіювати його, залежно від якоїсь умови), тоді const & все ще може бути кращим.

Наприклад, ви повинні зробити

void enqueue(std::shared<T> t) m_internal_queue.enqueue(std::move(t));

над

void enqueue(std::shared<T> const& t) m_internal_queue.enqueue(t);

Тому що в цьому випадку ви завжди створюєте копію всередині


1

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

Після мого результату тесту збільшення приросту та зменшення атомних int32 займає в 2 або 40 разів більше, ніж неатомний приріст та декремент. Я отримав це на 3GHz Core i7 з Windows 8.1. Перший результат виходить, коли не виникає суперечок, другий, коли виникає велика можливість суперечки. Я маю на увазі, що атомні операції, нарешті, є апаратним блокуванням. Замок - замок. Погані показники, коли виникають суперечки.

Переживаючи це, я завжди використовую byref (const shared_ptr &), ніж byval (shared_ptr).


1

Нещодавно з’явилось повідомлення в блозі: https://medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce

Тож відповідь на це така: чи (майже) ніколи не проходиш повз const shared_ptr<T>&.
Просто передайте базовий клас замість цього.

В основному єдиними розумними типами параметрів є:

  • shared_ptr<T> - Змініть та візьміть на себе право власності
  • shared_ptr<const T> - Не змінюйте, приймайте право власності
  • T& - Модифікувати, немає права власності
  • const T& - Не змінюйте, не володійте
  • T - Не змінюйте, не майте права власності, дешево копіюйте

Як @accel вказав у https://stackoverflow.com/a/26197326/1930508, порада від Herb Sutter:

Використовуйте const shared_ptr & як параметр лише в тому випадку, якщо ви не впевнені, чи будете ви копіювати та надавати право власності

Але в скільки випадків ви не впевнені? Тож це рідкісна ситуація


0

Відомо питання про те, що передача shared_ptr за значенням має вартість і слід уникати, якщо це можливо.

Вартість проїзду через shared_ptr

Більшу частину часу проходив би shared_ptr за посиланням, а ще краще за посиланням const.

Основна інструкція cpp має специфічне правило для передачі shared_ptr

R.34: Візьміть параметр shared_ptr, щоб висловити, що функція є власником частини

void share(shared_ptr<widget>);            // share -- "will" retain refcount

Приклад, коли передача shared_ptr за значенням дійсно необхідна, це те, коли абонент передає спільний об'єкт асинхронному виклику, тобто той, хто викликає, виходить за межі до того, як виклик завершить свою роботу. Оголошення повинне "продовжити" термін експлуатації спільного об'єкта, прийнявши share_ptr за значенням. У цьому випадку передавання посилання на shared_ptr не обійдеться.

Те саме стосується передачі спільного об'єкта в робочу нитку.


-4

shared_ptr недостатньо великий, а також його конструктор \ destructor не роблять достатньо роботи, щоб було достатньо накладних витрат з копії, щоб дбати про пропуск посилання на пропуск при виконанні копії.


15
Ви це виміряли?
curiousguy

2
@stonemetal: Що з атомними інструкціями під час створення нового спільного_ptr?
Квара

Це тип не POD, тому в більшості ABI навіть передача його "за значенням" насправді передає вказівник. Справа зовсім не в фактичному копіюванні байтів. Як видно з виводу ASM, передача a shared_ptr<int>за значенням займає понад 100 x86 інструкцій (включаючи дорогі lockінструкції ed для атомного включення / зменшення кількості посилань). Передача постійного ref - це те саме, що передача покажчика на що-небудь (і в цьому прикладі на провіднику компілятора Godbolt оптимізація хвостових викликів перетворює це на простий jmp замість виклику: godbolt.org/g/TazMBU ).
Пітер Кордес

TL: DR: Це C ++, де конструктори копій можуть зробити набагато більше роботи, ніж просто копіювати байти. Ця відповідь - загальне сміття.
Пітер Кордес

2
stackoverflow.com/questions/3628081/shared-ptr-horrible-speed Як приклад Спільні покажчики, передані за значенням та пропуском за посиланням, він бачить різницю в часі виконання приблизно 33%. Якщо ви працюєте над критичним кодом продуктивності, то голі покажчики отримують більше збільшення продуктивності. Тож обов'язково проходьте повз const ref, якщо ви пам’ятаєте, але це не є великою справою, якщо ви цього не зробите. Набагато важливіше не використовувати shared_ptr, якщо він вам не потрібен.
каменеметал
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.