Передача const shared_ptr <T> и по сравнению с shared_ptr <T> в качестве параметра
Я читал довольно много дискуссий о проблемах с производительностью, когда в приложении задействованы интеллектуальные указатели. Одной из частых рекомендаций является передача умного указателя как const & вместо копии, например:
void doSomething(std::shared_ptr<T> o) {}
против
void doSomething(const std::shared_ptr<T> &o) {}
Однако, разве второй вариант фактически не побеждает цель общего указателя? Мы на самом деле разделяем общий указатель здесь, поэтому, если по каким-то причинам указатель освобождается в вызывающем коде (подумайте о повторном включении или побочных эффектах), указатель const станет недействительным. Ситуация, которую должен предотвратить общий указатель. Я понимаю, что const & экономит некоторое время, так как нет копирования и блокировки для управления счетом. Но цена делает код менее безопасным, верно?
Ответы
Ответ 1
Преимущество прохождения shared_ptr
на const&
заключается в том, что счетчик ссылок не нужно увеличивать, а затем уменьшать. Поскольку эти операции должны быть потокобезопасными, они могут быть дорогими.
Вы совершенно правы, что существует риск того, что у вас может быть цепочка проходов по ссылке, которая позже лишает права главой цепочки. Это случилось со мной однажды в реальном проекте с реальными последствиями. Одна функция обнаружила shared_ptr
в контейнере и передала ссылку на нее по стеку вызовов. Функция, находящаяся в глубине стека вызовов, удаляла объект из контейнера, в результате чего все ссылки внезапно ссылались на объект, который больше не существовал.
Итак, когда вы передаете что-то по ссылке, вызывающий должен обеспечить, чтобы он выжил для жизни вызова функции. Не используйте пропуск по ссылке, если это проблема.
(Я предполагаю, что у вас есть прецедент, где есть какая-то конкретная причина для передачи по shared_ptr
, а не по ссылке. Наиболее распространенной причиной является то, что вызываемой функции может потребоваться продлить срок службы объекта. )
Обновление: некоторые подробности об ошибке для заинтересованных: в этой программе были объекты, которые были совместно использованы и реализовывали внутреннюю безопасность потоков. Они содержались в контейнерах, и для функций было распространено их продление.
Этот конкретный тип объекта может жить в двух контейнерах. Один, когда он был активным, и один, когда он был неактивным. Некоторые операции работали над активными объектами, некоторые - с неактивными объектами. Случай с ошибкой произошел, когда команда была получена от неактивного объекта, который сделал его активным, в то время как только shared_ptr
к объекту удерживался контейнером неактивных объектов.
Неактивный объект находился в контейнере. Ссылка на shared_ptr
в контейнере передавалась по ссылке на обработчик команд. Через цепочку ссылок этот shared_ptr
в конечном итоге попал в код, который понял, что это неактивный объект, который должен был быть активным. Объект был удален из неактивного контейнера (который уничтожил неактивный контейнер shared_ptr
) и добавлен в активный контейнер (который добавил другую ссылку на shared_ptr
, переданную в процедуру "добавить" ).
В этот момент было возможно, что единственным shared_ptr
для объекта, который существовал, был тот, который был в неактивном контейнере. Каждая другая функция в стеке вызовов просто ссылалась на нее. Когда объект был удален из неактивного контейнера, объект мог быть уничтожен, и все эти ссылки были в shared_ptr
, которые больше не существовали.
Потребовалось около месяца, чтобы распутать это.
Ответ 2
Прежде всего, не передавайте shared_ptr
вниз цепочку вызовов, если нет возможности, чтобы одна из вызываемых функций сохранила ее копию. Передайте ссылку на упомянутый объект или необработанный указатель на этот объект или, возможно, в поле, в зависимости от того, может ли он быть необязательным или нет.
Но когда вы передаете shared_ptr
, то предпочтительно передайте его по ссылке const
, так как копирование shared_ptr
имеет дополнительные накладные расходы. Копирование должно обновить общий счетчик ссылок, и это обновление должно быть потокобезопасным. Следовательно, существует небольшая неэффективность, которую можно (безопасно) избегать.
Относительно
" цена делает код менее безопасным, не так ли?
Нет. Цена является дополнительным указателем на наивно сгенерированный машинный код, но компилятор справляется с этим. Итак, все, что нужно, просто избегать незначительных, но совершенно ненужных накладных расходов, которые компилятор не может оптимизировать для вас, если он не супер-умный.
Как объяснил Дэвид Шварц в своем ответе, когда вы передаете ссылку на const
проблему <сглаживания , где вызываемая вами функция в свою очередь изменяет или вызывает функцию, которая изменяет исходный объект, возможное. А по закону Мерфи это произойдет в самое неудобное время, с максимальной стоимостью, и с самым запутанным непроницаемым кодом. Но это так, независимо от того, является ли аргумент string
или shared_ptr
или что-то еще. К счастью, это очень редкая проблема. Но имейте это в виду, также для передачи shared_ptr
экземпляров.
Ответ 3
Прежде всего, существует семантическая разница между ними:
передача общего указателя по значению означает, что ваша функция будет принимать участие в владении базовым объектом.
Передача shared_ptr в качестве ссылки на const не указывает на какой-либо смысл поверх простого пропускания базового объекта по ссылке const (или необработанному указателю), кроме того, что пользовательские функции этой функции используют shared_ptr. Так что в основном мусор.
Сравнение влияния производительности на них не имеет значения, если они семантически отличаются.
из https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/
Не передавать интеллектуальный указатель в качестве параметра функции, если вы не хотите использовать или манипулировать самим интеллектуальным указателем, например, для совместного использования или передать право собственности.
и на этот раз я полностью согласен с Herb:)
И еще одна цитата из того же самого, которая напрямую отвечает на вопрос
Рекомендация: используйте не-const shared_ptr & параметр только для изменения shared_ptr. Используйте константу shared shared_ptr & как параметр только в том случае, если вы не уверены, возьмете ли вы копию и долей собственности; в противном случае используйте * вместо (или, если не nullable, a &)
Ответ 4
Я думаю, что его разумно пройти через const &
, если целевая функция синхронна и только использует параметр во время выполнения и больше не нуждается в ней при возврате. Здесь разумно экономить на стоимости увеличения ссылочного счета - поскольку вам не нужна дополнительная безопасность в этих ограниченных обстоятельствах - если вы понимаете последствия и уверены, что код в безопасности.
Это в противоположность тому, когда функция должна сохранять параметр (например, в члене класса) для последующего повторного ссылки.
Ответ 5
Если в вашем методе не применяется модификация владения, нет никакой пользы для вашего метода взять shared_ptr путем копирования или ссылки на const, он загрязняет API и потенциально может нанести накладные расходы (при передаче по копии)
Чистым способом является передача базового типа с помощью ref ref или ref в зависимости от вашего использования.
void doSomething(const T& o) {}
auto s = std::make_shared<T>(...);
// ...
doSomething(*s);
Основной указатель не может быть освобожден во время вызова метода
Ответ 6
Как указано в С++ - shared_ptr: ужасная скорость, копирование shared_ptr
требует времени. Конструкция включает в себя атомный приращение и разрушение атомного декремента, атомное обновление (независимо от того, увеличиваются или уменьшаются) могут препятствовать ряду оптимизаций компилятора (загрузки/хранения памяти не могут мигрировать по операции), а на аппаратном уровне используется протокол согласованности кэша ЦП чтобы убедиться, что вся строка кеша находится в собственности (эксклюзивный режим) с помощью ядра, делающего модификацию.
Итак, вы правы, std::shared_ptr<T> const&
может использоваться как улучшение производительности только за std::shared_ptr<T>
.
Вы также правы, что существует теоретический риск того, что указатель/ссылка станет болтаться из-за некоторого сглаживания.
При этом риск скрыт в любой программе на С++: любое использование указателя или ссылки является риском. Я бы сказал, что несколько случаев появления std::shared_ptr<T> const&
должны быть каплей воды по сравнению с общим числом применений T&
, T const&
, T*
,...
Наконец, я хотел бы указать, что прохождение a shared_ptr<T> const&
является странным. Возможны следующие случаи:
-
shared_ptr<T>
: Мне нужна копия shared_ptr
-
T*
/T const&
/T&
/T const&
: Мне нужен (возможно, нулевой) дескриптор для T
Следующий случай гораздо реже:
-
shared_ptr<T>&
: я могу перезагрузить shared_ptr
Но прохождение shared_ptr<T> const&
? Легитимное использование очень редко.
Передача shared_ptr<T> const&
, где все, что вы хотите, является ссылкой на T
, является анти-шаблоном: вы вынуждаете пользователя использовать shared_ptr
, когда они могут выделять T
другим способом! В большинстве случаев (99,99..%) вам не важно, как выделяется T
.
Единственный случай, когда вы передадите shared_ptr<T> const&
, - это если вы не уверены, нужна ли вам копия или нет, а потому, что вы профилировали программу и показали, что этот атомный прирост/декремент был узким местом, вы решили отложить создание копии только в тех случаях, когда это необходимо.
Это такой крайний случай, что любое использование shared_ptr<T> const&
следует рассматривать с наивысшей степенью подозрений.