С++ - передача ссылок на std:: shared_ptr или boost:: shared_ptr

Если у меня есть функция, которая должна работать с shared_ptr, не было бы более эффективным передать ей ссылку на нее (чтобы избежать копирования объекта shared_ptr)? Каковы возможные плохие побочные эффекты? Я предполагаю два возможных случая:

1) внутри функции копируется из аргумента, как в

ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp)  
{  
     ...  
     m_sp_member=sp; //This will copy the object, incrementing refcount  
     ...  
}  

2) внутри функции используется только аргумент, например, в

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}  

В обоих случаях я не вижу причин для передачи boost::shared_ptr<foo> по значению вместо ссылки. Передача по значению только "временно" увеличивает счетчик ссылок из-за копирования, а затем уменьшает его при выходе из области действия. Я что-то пропускаю?

Просто, чтобы прояснить, прочитав несколько ответов: я полностью согласен с проблемами преждевременной оптимизации, и я всегда стараюсь использовать профили первого профиля, а затем-работать-на-hotspots. Мой вопрос был больше из чисто технического кода-точки зрения, если вы знаете, что я имею в виду.

Ответ 1

Точка отдельного экземпляра shared_ptr должна гарантировать (насколько это возможно), что пока этот shared_ptr находится в области видимости, объект, на который он указывает, будет по-прежнему существовать, поскольку его счетчик ссылок будет по меньшей мере 1.

Class::only_work_with_sp(boost::shared_ptr<foo> sp)
{
    // sp points to an object that cannot be destroyed during this function
}

Итак, используя ссылку на shared_ptr, вы отключите эту гарантию. Итак, во втором случае:

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}

Откуда вы знаете, что sp->do_something() не взорвется из-за нулевого указателя?

Все зависит от того, что находится в этих "..." разделах кода. Что делать, если вы вызываете что-то во время первого "...", который имеет побочный эффект (где-то в другой части кода) для очистки объекта shared_ptr от того же объекта? И что, если это единственный единственный выделенный shared_ptr для этого объекта? Пока, пока вы пытаетесь его использовать.

Итак, есть два способа ответить на этот вопрос:

  • Осмотрите источник всей вашей программы очень внимательно, пока не убедитесь, что объект не умрет во время тела функции.

  • Измените параметр назад как отдельный объект вместо ссылки.

Общая информация о рекомендациях, которые применяются здесь: не беспокойтесь о том, чтобы делать рискованные изменения в вашем коде для достижения производительности, пока вы не приурочили свой продукт в реалистичной ситуации в профилировщике и не сделали окончательно определенным, что изменения, которые вы хотите сделать существенно повлияет на производительность.

Обновление для комментатора JQ

Здесь надуманный пример. Это преднамеренно просто, так что ошибка будет очевидна. В реальных примерах ошибка не так очевидна, потому что она скрыта в слоях реальной детали.

У нас есть функция, которая отправит сообщение где-нибудь. Это может быть большое сообщение, а не использовать std::string, который, вероятно, будет скопирован, поскольку он передан в несколько мест, мы используем shared_ptr для строки:

void send_message(std::shared_ptr<std::string> msg)
{
    std::cout << (*msg.get()) << std::endl;
}

(Мы просто "отправляем" его на консоль для этого примера).

Теперь мы хотим добавить средство для запоминания предыдущего сообщения. Нам нужно следующее поведение: должна существовать переменная, которая содержит самое последнее отправленное сообщение, но пока сообщение отправляется, тогда не должно быть предыдущего сообщения (перед отправкой должна быть reset). Итак, мы объявляем новую переменную:

std::shared_ptr<std::string> previous_message;

Затем мы изменяем нашу функцию в соответствии с указанными нами правилами:

void send_message(std::shared_ptr<std::string> msg)
{
    previous_message = 0;
    std::cout << *msg << std::endl;
    previous_message = msg;
}

Итак, прежде чем мы начнем отправлять, мы отменим текущее предыдущее сообщение, а затем после завершения отправки мы можем сохранить новое предыдущее сообщение. Все хорошо. Вот несколько тестовых кодов:

send_message(std::shared_ptr<std::string>(new std::string("Hi")));
send_message(previous_message);

И как и ожидалось, это дважды печатает Hi!.

Теперь идет мистер Мейнтейнер, который смотрит на код и думает: Эй, этот параметр send_message есть shared_ptr:

void send_message(std::shared_ptr<std::string> msg)

Очевидно, что можно изменить на:

void send_message(const std::shared_ptr<std::string> &msg)

Подумайте о повышении производительности, которое это принесет! (Не обращайте внимания на то, что мы собираемся отправить типично большое сообщение по какому-либо каналу, поэтому повышение производительности будет настолько маленьким, чтобы быть непередаваемым).

Но реальная проблема заключается в том, что теперь тестовый код будет демонстрировать поведение undefined (в сборках отладки Visual С++ 2010, он сбой).

Mr Maintainer удивлен этим, но добавляет защитную проверку на send_message в попытке остановить проблему:

void send_message(const std::shared_ptr<std::string> &msg)
{
    if (msg == 0)
        return;

Но, конечно, он все еще продолжается и сбой, потому что msg никогда не является нулевым при вызове send_message.

Как я уже сказал, со всем кодом, настолько близким вместе в тривиальном примере, легко найти ошибку. Но в реальных программах с более сложными отношениями между изменяемыми объектами, которые содержат указатели друг на друга, легко сделать ошибку и сложно построить необходимые тестовые примеры для обнаружения ошибки.

Простое решение, в котором вы хотите, чтобы функция могла полагаться на shared_ptr, которая продолжала оставаться непустой в течение всего времени, заключается в том, что функция выделяет свой собственный true shared_ptr, а не полагается на ссылку на существующий shared_ptr.

Недостатком является то, что скопирован a shared_ptr не является бесплатным: даже "незакрепленные" реализации должны использовать взаимосвязанную операцию для соблюдения гарантий потоковой передачи. Таким образом, могут быть ситуации, когда программа может быть значительно ускорена, изменив shared_ptr на shared_ptr &. Но это не изменение, которое можно безопасно сделать для всех программ. Он изменяет логический смысл программы.

Обратите внимание, что подобная ошибка возникла бы, если бы мы использовали std::string повсюду вместо std::shared_ptr<std::string>, а вместо:

previous_message = 0;

чтобы очистить сообщение, мы сказали:

previous_message.clear();

Тогда симптомом будет случайная отправка пустого сообщения вместо поведения undefined. Стоимость дополнительной копии очень большой строки может быть намного более значительной, чем стоимость копирования shared_ptr, поэтому компромисс может быть другим.

Ответ 2

Я обнаружил, что не согласен с наивысшим голосованием, поэтому я пошел искать экспертов и вот они. Из http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything

Herb Sutter: "когда вы передаете shared_ptr, копии дороги"

Скотт Мейерс: "Нет ничего особенного в shared_ptr, когда речь заходит о том, передаете ли вы его по значению или передаете его по ссылке. Используйте тот же анализ, который вы используете для любого другого определенного пользователем типа. У людей, похоже, есть такое восприятие, shared_ptr каким-то образом решает все проблемы управления, а потому, что он мал, он обязательно недорого переносить по значению. Он должен быть скопирован, и есть стоимость, связанная с этим... дорого переносить его по стоимости, поэтому, если я могу уйди с ним с правильной семантикой в ​​моей программе, я собираюсь передать ее ссылкой на константу или ссылку вместо этого

Herb Sutter: "всегда передавайте их по ссылке на const, и очень часто, возможно, потому, что вы знаете, что вы назвали, может изменить то, с чем вы получили ссылку, может быть, тогда вы можете пройти по значению... если вы скопируете их как параметры, о, моя доброта, вам почти никогда не нужно бить этот счетчик ссылок, потому что он все равно остается живым, и вы должны передавать его по ссылке, поэтому, пожалуйста, сделайте это"

Обновление: Herb расширился на этом здесь: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/, хотя мораль этой истории заключается в том, что вы не должны пропускать shared_ptr на всех ", если вы не хотите использовать или манипулировать самим интеллектуальным указателем, например, для совместного использования или передачи прав собственности."

Ответ 3

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

Во-первых, вы не представляете, как будет развиваться интерфейс для вашего класса, и вы хотите, чтобы другие программисты не делали плохие вещи. Передача shared_ptr по ссылке не является чем-то, что программист должен ожидать увидеть, потому что это не идиоматично, и это упрощает его использование неправильно. Защищайте программу: трудно использовать интерфейс. Передача по ссылке просто вызовет проблемы позже.

Во-вторых, не оптимизируйте, пока не узнаете, что этот конкретный класс будет проблемой. Сначала профиль, а затем, если ваша программа действительно нуждается в усилении, полученном путем передачи по ссылке, то, возможно. В противном случае не потейте мелкие вещи (то есть дополнительные N инструкций, которые нужно пройти по значению) вместо этого беспокоятся о дизайне, структурах данных, алгоритмах и долгосрочной ремонтопригодности.

Ответ 4

Да, там хорошая ссылка. Вы не намерены предоставлять метод совместного владения; он только хочет работать с ним. Вы можете взять ссылку на первый случай, так как вы все равно копируете его. Но для первого случая он приобретает право собственности. Этот трюк все равно копирует его только один раз:

void ClassA::take_copy_of_sp(boost::shared_ptr<foo> sp) {
    m_sp_member.swap(sp);
}

Вы также должны скопировать, когда вы его вернете (т.е. не вернете ссылку). Потому что ваш класс не знает, что делает с ним клиент (он может хранить указатель на него, а затем происходит большой шум). Если позже это станет узким местом (первый профиль!), Вы все равно можете вернуть ссылку.


Изменить. Конечно, как отмечают другие, это справедливо только в том случае, если вы знаете свой код и знаете, что вы не передали reset переданный общий указатель. Если у вас есть сомнения, просто перейдите по значению.

Ответ 5

Разумно передать shared_ptr на const&. Это вряд ли вызовет проблемы (за исключением маловероятного случая, когда ссылочный shared_ptr будет удален во время вызова функции, как описано в Earwicker), и это, скорее всего, будет быстрее, если вы передадите много из них. Запомнить; по умолчанию boost::shared_ptr является потокобезопасным, поэтому копирование включает в себя потоковое безопасное приращение.

Попробуйте использовать const&, а не только &, потому что временные объекты могут не передаваться с помощью неконстантной ссылки. (Несмотря на то, что расширение языка в MSVC позволяет делать это в любом случае)

Ответ 6

Во втором случае это проще:

Class::only_work_with_sp(foo &sp)
{    
    ...  
    sp.do_something();  
    ...  
}

Вы можете назвать его

only_work_with_sp(*sp);

Ответ 7

Я бы избегал "простой" ссылки, если функция явно не может изменить указатель.

A const & может быть разумной микрооптимизацией при вызове небольших функций - например, чтобы обеспечить дальнейшую оптимизацию, например, отделить некоторые условия. Кроме того, приращение/уменьшение - поскольку это потокобезопасность - это точка синхронизации. Я бы не ожидал, что это сильно повлияет на большинство сценариев.

Как правило, вы должны использовать более простой стиль, если у вас нет причин. Затем либо последовательно используйте const &, либо добавьте комментарий о том, почему, если вы используете его только в нескольких местах.

Ответ 8

Я бы отстаивал передачу общего указателя по ссылке const - семантику, согласно которой функция, передаваемая с указателем, НЕ владеет указателем, что является чистой идиомой для разработчиков.

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

Передача общего указателя с помощью ссылки, отличной от const, иногда является опасной - причиной являются функции swap и reset, которые функция может вызывать внутри, чтобы уничтожить объект, который по-прежнему считается действительным после возвращения функции.

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

Только мои 2 цента: -)

Ответ 9

Кажется, что все плюсы и минусы здесь могут быть фактически обобщены на ЛЮБОЙ тип, переданный по ссылке не только shared_ptr. На мой взгляд, вы должны знать семантику передачи по ссылке, константу ссылки и значение и использовать ее правильно. Но абсолютно ничего не происходит из-за неправильной передачи shared_ptr по ссылке, если вы не считаете, что все ссылки плохие...

Чтобы вернуться к примеру:

Class::only_work_with_sp( foo &sp ) //Again, no copy here  
{    
    ...  
    sp.do_something();  
    ...  
}

Откуда вы знаете, что sp.do_something() не взорвется из-за оборванного указателя?

Истина заключается в том, что shared_ptr или нет, const или нет, это может произойти, если у вас есть дефект дизайна, например, прямо или косвенно, разделение владения sp между потоками, пропуская объект, который делает delete this, вы имеют круговую собственность или другие ошибки собственности.

Ответ 10

Одна вещь, о которой я еще не упоминал, заключается в том, что когда вы передаете общие указатели по ссылке, вы теряете неявное преобразование, которое вы получаете, если хотите передать общий указатель с производным классом через ссылку на общий указатель базового класса.

Например, этот код приведет к ошибке, но он будет работать, если вы измените test(), чтобы общий указатель не передавался по ссылке.

#include <boost/shared_ptr.hpp>

class Base { };
class Derived: public Base { };

// ONLY instances of Base can be passed by reference.  If you have a shared_ptr
// to a derived type, you have to cast it manually.  If you remove the reference
// and pass the shared_ptr by value, then the cast is implicit so you don't have
// to worry about it.
void test(boost::shared_ptr<Base>& b)
{
    return;
}

int main(void)
{
    boost::shared_ptr<Derived> d(new Derived);
    test(d);

    // If you want the above call to work with references, you will have to manually cast
    // pointers like this, EVERY time you call the function.  Since you are creating a new
    // shared pointer, you lose the benefit of passing by reference.
    boost::shared_ptr<Base> b = boost::dynamic_pointer_cast<Base>(d);
    test(b);

    return 0;
}

Ответ 11

Я предполагаю, что вы знакомы с преждевременной оптимизацией и спрашиваете об этом либо в академических целях, либо потому, что вы выделили некоторые пре- существующий код, который не работает.

Передача по ссылке в порядке

Передача с помощью ссылки const лучше и обычно может быть использована, поскольку она не вынуждает const-ness к указанному объекту.

Вы не рискуете потерять указатель из-за использования ссылки. Эта ссылка является доказательством того, что у вас есть копия умного указателя ранее в стеке, и только один поток владеет стеком вызовов, так что уже существовавшая копия не исчезает.

Использование ссылок часто более эффективно по причинам, о которых вы упоминаете, , но не гарантировано. Помните, что разыменование объекта может также занять работу. Ваш идеальный сценарий использования ссылок будет, если ваш стиль кодирования включает в себя множество небольших функций, где указатель будет передаваться от функции к функции, чтобы функционировать до ее использования.

Вы должны всегда избегать хранения вашего умного указателя в качестве ссылки. Ваш пример Class::take_copy_of_sp(&sp) показывает правильное использование для этого.

Ответ 12

Предполагая, что нас не интересует константная корректность (или больше, вы имеете в виду позволить функциям изменять или передавать права собственности на передаваемые данные), передача boost:: shared_ptr по значению безопаснее, чем передача его по ссылке, поскольку мы разрешаем исходный boost:: shared_ptr контролировать его собственное время жизни. Рассмотрим результаты следующего кода...

void FooTakesReference( boost::shared_ptr< int > & ptr )
{
    ptr.reset(); // We reset, and so does sharedA, memory is deleted.
}

void FooTakesValue( boost::shared_ptr< int > ptr )
{
    ptr.reset(); // Our temporary is reset, however sharedB hasn't.
}

void main()
{
    boost::shared_ptr< int > sharedA( new int( 13 ) );
    boost::shared_ptr< int > sharedB( new int( 14 ) );

    FooTakesReference( sharedA );

    FooTakesValue( sharedB );
}

В приведенном выше примере мы видим, что передача sharedA по ссылке позволяет FooTakesReference с reset исходным указателем, что уменьшает его использование до 0, уничтожая его. Однако FooTakesValue не может reset исходный указатель, гарантируя, что данные sharedB все еще пригодны для использования. Когда другой разработчик неизбежно приходит и пытается совместить с хрупким существованием sharedA, возникает хаос. Однако счастливый разработчик sharedB отправляется домой рано, так как все правильно в его мире.

Безопасность кода в этом случае намного превосходит любое копирование улучшения скорости. В то же время boost:: shared_ptr призван улучшить безопасность кода. Гораздо проще перейти от копии к ссылке, если что-то требует такой оптимизации ниши.

Ответ 13

Sandy писал: "Кажется, что все плюсы и минусы здесь могут быть фактически обобщены на ЛЮБОЙ тип, переданный по ссылке не только shared_ptr."

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

Я написал "почти" в предыдущем предложении, потому что производительность может быть проблемой, и это может быть оправдано в редких случаях, но я бы тоже избегал этого сценария и сам искал все возможные другие решения оптимизации, такие как серьезно посмотреть на добавление другого уровня косвенности, ленивой оценки и т.д.

Код, который существует за его автором или даже публикует его память автора, который требует неявных предположений о поведении, в частности, поведение в отношении жизни объектов, требует четкой, сжатой, доступной для чтения документации, а затем многие клиенты все равно ее не прочитают! Простота почти всегда превосходит эффективность, и почти всегда есть другие способы быть эффективными. Если вам действительно нужно передавать значения по ссылке, чтобы избежать глубокого копирования конструкторами копирования ваших объектов с подсчетом ссылок (и оператора equals), то, возможно, вам следует рассмотреть способы сделать глубинные скопированные данные ссылками на подсчитанные указатели, которые могут быть скопировано быстро. (Конечно, это только один сценарий проектирования, который может не соответствовать вашей ситуации).

Ответ 14

Я использовал для работы в проекте, что принцип был очень сильным в отношении передачи интеллектуальных указателей по значению. Когда меня попросили провести анализ производительности, я обнаружил, что для приращения и уменьшения опорных счетчиков интеллектуальных указателей приложение занимает от 4 до 6% используемого процессорного времени.

Если вы хотите передать интеллектуальные указатели по значению, просто чтобы избежать проблем в странных случаях, как описано у Daniel Earwicker, убедитесь, что вы понимаете цену, которую вы платите за нее.

Если вы решите пойти со ссылкой, основной причиной использования ссылки const является возможность иметь неявное повышение, когда вам нужно передать общий указатель на объект из класса, который наследует класс, который вы используете в интерфейсе.

Ответ 15

В дополнение к тому, что сказал litb, я хотел бы указать, что, вероятно, это будет проходить по ссылке const во втором примере, так что вы уверены, что не произвольно изменяете его.

Ответ 16

struct A {
  shared_ptr<Message> msg;
  shared_ptr<Message> * ptr_msg;
}
  • передать по значению:

    void set(shared_ptr<Message> msg) {
      this->msg = msg; /// create a new shared_ptr, reference count will be added;
    } /// out of method, new created shared_ptr will be deleted, of course, reference count also be reduced;
    
  • передать по ссылке:

    void set(shared_ptr<Message>& msg) {
     this->msg = msg; /// reference count will be added, because reference is just an alias.
     }
    
  • передать указатель:

    void set(shared_ptr<Message>* msg) {
      this->ptr_msg = msg; /// reference count will not be added;
    }
    

Ответ 17

Каждый фрагмент кода должен иметь некоторый смысл. Если вы передаете общий указатель по значению везде в приложении, это означает: "Я не уверен, что происходит в другом месте, поэтому я предпочитаю безопасную безопасность". Это не то, что я называю знаком хорошей уверенности другим программистам, которые могли бы проконсультироваться с кодом.

В любом случае, даже если функция получает ссылку на константу, и вы "неуверены", вы все равно можете создать копию общего указателя в начале функции, чтобы добавить сильную ссылку на указатель. Это также можно рассматривать как намек на дизайн ( "указатель может быть изменен в другом месте" ).

Итак, да, IMO, по умолчанию должен быть " pass by const reference".