C++ - передача ссылок на std:: shared ptr или boost:: shared ptr

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

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> по значению а не по ссылке. Передача по значению только "временно" увеличит счетчик ссылок из-за копирования, а затем уменьшит его при выходе из области функции. Я что-то упускаю?

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

17 ответов


точка отчетливого 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 к тому же объекту? А что, если это единственное оставшееся различение?--11--> к этому объекту? Пока-пока объект, только там, где вы собираетесь попробовать и использовать его.

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

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

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

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

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

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

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

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

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

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

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)

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

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

г-н сопровождающий удивлен этим, но добавляет защитный чек к send_message in попытка остановить проблему происходит:

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

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

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

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

недостатком является то, что скопировал shared_ptr не свободно: даже "lock-free" реализации должны использовать блокировать деятельность для того чтобы почтить продевать нитку гарантии. Поэтому могут возникнуть ситуации, когда программа можно значительно ускорить, изменив a shared_ptr на shared_ptr &. Но это не то изменение, которое можно безопасно внести во все программы. Это меняет логическое значение программы.

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

previous_message = 0;

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

previous_message.clear();

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


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

Херб Саттер: "когда вы проходите shared_ptr, копии дорогие"

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

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

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


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

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

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


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

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

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


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


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

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


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

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

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

only_work_with_sp(*sp);

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

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

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


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

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

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

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

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


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

вернуться к примеру:

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

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

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


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

например, этот код приведет к ошибке, но он будет работать, если вы измените 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;
}

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

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

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

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

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

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


предполагая, что мы не обеспокоены корректностью const (или более того, вы имеете в виду, чтобы функции могли изменять или совместно владеть передаваемыми данными), передача 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 чтобы сбросить исходный указатель, который уменьшает его количество использования до 0, уничтожая его данные. FooTakesValue, однако, не может сбросить исходный указатель, гарантируя sharedB это!--5--> данные по-прежнему можно использовать. Когда другой разработчик неизбежно приходит и пытается вернуться на sharedA это!--5--> хрупкое существование, наступает хаос. Счастливый sharedB разработчик, однако, идет домой рано, как все в порядке в его мир.

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


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

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

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

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


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

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

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


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


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

    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;
    
  2. проходим по ссылке:

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

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

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

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

Так что да, ИМО, по умолчанию должно быть "пройти по ссылке const".