C ++ - передача ссылок на std :: shared_ptr или boost :: shared_ptr - PullRequest
113 голосов
/ 29 ноября 2008

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

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

Ответы [ 17 ]

111 голосов
/ 13 января 2012

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

Херб Саттер: «когда вы передаете shared_ptr, копии стоят дорого» * ​​1004 *

Скотт Мейерс: «В shared_ptr нет ничего особенного, когда речь идет о том, передаете ли вы его по значению или передаете по ссылке. Используйте точно такой же анализ, какой вы используете для любого другого определенного пользователем типа. Люди, кажется, имеют такое представление, что shared_ptr каким-то образом решает все проблемы управления, и потому, что он небольшой, его обязательно нужно передавать по значению. Его нужно копировать, и с этим связаны издержки ... его стоит передавать по значению, так что если я смогу уйдите с этим с правильной семантикой в ​​моей программе, я вместо этого передам это ссылкой на const или reference "

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

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

107 голосов
/ 30 ноября 2008

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

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

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

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

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

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

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

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

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, пытаясь остановить возникновение проблемы:

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

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

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

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

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

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

previous_message = 0;

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

previous_message.clear();

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

22 голосов
/ 29 ноября 2008

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

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

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

18 голосов
/ 29 ноября 2008

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

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

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


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

11 голосов
/ 30 ноября 2008

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

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

10 голосов
/ 29 ноября 2008

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

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

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

only_work_with_sp(*sp);
3 голосов
/ 29 ноября 2010

Кажется, что все плюсы и минусы здесь могут быть обобщены на ЛЮБОЙ тип, передаваемый по ссылке, а не просто 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 иметь круговое владение или другие ошибки владения.

3 голосов
/ 28 марта 2010

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

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

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

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

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

3 голосов
/ 29 ноября 2008

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

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

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

2 голосов
/ 26 сентября 2014

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

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