Должны ли мы передавать shared_ptr по ссылке или по значению?

270

Когда функция берет shared_ptr(из boost или C ++ 11 STL), вы передаете ее:

  • по постоянной ссылке: void foo(const shared_ptr<T>& p)

  • или по значению void foo(shared_ptr<T> p):?

Я бы предпочел первый метод, потому что подозреваю, что он будет быстрее. Но стоит ли это того или есть дополнительные проблемы?

Не могли бы вы привести причины вашего выбора или, если дело, почему вы думаете, что это не имеет значения.

Danvil
источник
14
Проблема в том, что они не эквивалентны. Эталонная версия кричит «Я собираюсь псевдоним некоторых shared_ptr, и я могу изменить его, если я хочу.», В то время как версия значения говорит: «Я собираюсь скопировать ваш shared_ptr, так что, пока я могу изменить его, вы никогда не узнаете. Параметр const-reference - это реальное решение, которое гласит: «Я собираюсь использовать псевдоним для некоторых shared_ptr, и я обещаю не менять его» (что очень похоже на семантику по значению!)
GManNickG
2
Эй , я был бы заинтересован в ваших ребятах мнения о возвращении к shared_ptrчлену класса. Вы делаете это const-refs?
Йоханнес Шауб -
Третья возможность заключается в использовании std :: move () с C ++ 0x, это меняет оба shared_ptr
Tomaka17
@Johannes: Я бы вернул его по const-ссылке, чтобы избежать копирования / повторного подсчета. С другой стороны, я возвращаю всех членов по константной ссылке, если они не примитивны.
GManNickG
возможный дубликат C ++ - указатель прохождения вопроса
kennytm

Ответы:

229

Этот вопрос был обсужден и получен ответом Скотта, Андрея и Херба во время сессии « Спросите нас что-нибудь» на C ++ и после 2011 года . Смотрите с 4:34 о shared_ptrпроизводительности и правильности .

Короче говоря, нет смысла передавать по значению, если только цель не состоит в том, чтобы разделить владение объектом (например, между разными структурами данных или между разными потоками).

Если вы не можете переместить-оптимизировать это, как объяснено Скоттом Мейерсом в видео-ролике, связанном выше, но это связано с реальной версией C ++, которую вы можете использовать.

Главное обновление этой дискуссии произошло во время интерактивной панели конференции GoingNative 2012 : «Спросите нас о чем угодно» который стоит посмотреть, особенно с 22:50 .

mloskot
источник
5
но, как показано здесь, дешевле передавать по значению: stackoverflow.com/a/12002668/128384 не следует принимать это во внимание также (по крайней мере, для аргументов конструктора и т. д., где shared_ptr будет сделан членом класс)?
stijn
2
@stijn Да и нет. Упомянутые вами вопросы и ответы являются неполными, если они не разъясняют версию стандарта C ++, на которую они ссылаются. Очень легко распространять общие правила никогда / всегда, которые просто вводят в заблуждение. Если только читатели не потратят время на то, чтобы ознакомиться со статьей и ссылками Дэвида Абрахамса, или принять во внимание дату публикации и текущий стандарт C ++. Итак, оба ответа, мой и тот, на который вы указали, верны, учитывая время публикации.
mloskot
1
« если нет многопоточности » нет, MT ни в коем случае не является особенным.
любопытный парень
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 по значению.

Рекомендация: используйте неконстантный параметр shared_ptr & только для изменения shared_ptr. Используйте const shared_ptr & в качестве параметра только в том случае, если вы не уверены, будете ли вы брать копию и передавать ее в собственность; в противном случае используйте вместо него виджет * (или, если не обнуляемый, виджет &).

ACEL
источник
3
Спасибо за ссылку на Саттера. Это отличная статья. Я не согласен с ним в виджете *, предпочитая необязательный <widget &>, если доступен C ++ 14. widget * слишком двусмысленный из старого кода.
одноименный
3
+1 за включение виджета * и виджета & как возможности. Просто для уточнения, передача виджета * или виджета &, вероятно, является наилучшим вариантом, когда функция не проверяет / не изменяет сам объект указателя. Интерфейс является более общим, так как он не требует определенного типа указателя, и проблема производительности подсчета ссылок shared_ptr уклоняется.
Tgnottingham
4
Я думаю, что это должен быть принятый ответ сегодня, из-за второго руководства. Это явно лишает законной силы текущий принятый ответ, который говорит: нет никакой причины передать ценность.
'13
62

Лично я бы использовал constссылку. Нет необходимости увеличивать счетчик ссылок, чтобы просто уменьшить его снова для вызова функции.

Эван Теран
источник
1
Я не проголосовал против вашего ответа, но прежде чем это станет вопросом предпочтения, у каждой из двух возможностей есть свои плюсы и минусы. И было бы хорошо узнать и обсудить эти плюсы и минусы. После этого каждый может принять решение за себя.
Данвил
@Danvil: принимая во внимание, как shared_ptrработает, единственный возможный недостаток, чтобы не перейти по ссылке, это небольшая потеря в производительности. Здесь есть две причины. a) функция наложения указателей означает, что копируется значение данных указателей плюс счетчик (возможно, 2 для слабых ссылок), поэтому копирование данных обходится немного дороже. b) атомарный подсчет ссылок немного медленнее, чем обычный старый код приращения / уменьшения, но необходим для обеспечения безопасности потоков. Кроме того, эти два метода одинаковы для большинства целей и задач.
Эван Теран
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, обновил ответ, чтобы теперь использовать сборку релиза.
УТС
1
Мне любопытно, если при включенной оптимизации вы получите одинаковые результаты с обоими
Эллиот Вудс
2
Оптимизация не сильно помогает. проблема заключается в конфликте блокировок на счетчике ссылок на копии.
Алекс
1
Не в этом дело. Такая foo()функция вообще не должна даже принимать общий указатель, потому что она не использует этот объект: она должна принимать int&и делать p = ++x;, вызывая foo(*p);из main(). Функция принимает объект умного указателя, когда ему нужно что-то с ним сделать, и в большинстве случаев вам нужно переместить его ( std::move()) в другое место, поэтому параметр по значению не требует затрат.
Eepp
15

Начиная с C ++ 11, вы должны воспринимать его по значению над const и чаще, чем вы думаете.

Если вы берете std :: shared_ptr (а не базовый тип T), то вы делаете это, потому что хотите что-то с ним сделать.

Если вы хотите скопировать его куда-то, имеет смысл взять его копией и std :: переместить его внутренне, а не копировать с помощью const &, а затем скопировать позже. Это потому, что вы позволяете вызывающей опции в свою очередь вызывать std :: move 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).

Hyunjik Bae
источник
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, совет Херба Саттера:

Используйте const shared_ptr & в качестве параметра, только если вы не уверены, будете ли вы брать копию и делиться правами собственности

Но в скольких случаях вы не уверены? Так что это редкая ситуация

Flamefire
источник
0

Известна проблема, заключающаяся в том, что передача shared_ptr по значению имеет цену, и ее следует избегать, если это возможно.

Стоимость проезда по shared_ptr

Большую часть времени можно было бы передавать shared_ptr по ссылке, а еще лучше по ссылке.

В основном руководстве по cpp есть специальное правило для передачи shared_ptr

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

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

Пример, когда передача shared_ptr по значению действительно необходима, - это когда вызывающий объект передает общий объект асинхронному вызываемому объекту, т. Е. Вызывающий объект выходит из области видимости, прежде чем вызываемый объект завершит свою работу. Вызываемый должен «продлить» время жизни разделяемого объекта, приняв share_ptr по значению. В этом случае передача ссылки на shared_ptr не подходит.

То же самое касается передачи общего объекта в рабочий поток.

ARTM
источник
-4

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

stonemetal
источник
15
Вы измерили это?
любопытный парень
2
@stonemetal: А как насчет атомарных инструкций при создании нового shared_ptr?
Quarra
Это не POD-тип, поэтому в большинстве ABI, даже передавая его «по значению», фактически передает указатель. Проблема не в копировании байтов. Как вы можете видеть в выводе asm, передача shared_ptr<int>значения by занимает более 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, если он вам не нужен.
каменный металл