Как вернуть интеллектуальные указатели (shared_ptr) по ссылке или по значению?

95

Допустим, у меня есть класс с методом, который возвращает shared_ptr.

Каковы возможные преимущества и недостатки возврата по ссылке или по значению?

Две возможные подсказки:

  • Раннее разрушение объекта. Если я возвращаю shared_ptrссылку by (const), счетчик ссылок не увеличивается, поэтому я рискую удалить объект, когда он выходит за пределы области видимости в другом контексте (например, в другом потоке). Это верно? Что, если среда однопоточная, может ли такая ситуация случиться?
  • Стоимость. Конечно, передача по стоимости не бесплатна. Стоит ли этого избегать по возможности?

Спасибо всем.

Винченцо Пии
источник

Ответы:

115

Возвращать интеллектуальные указатели по значению.

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

Озабоченность стоимости в настоящее время спорные благодаря оптимизации возвращаемого значения (РВО), так что вы не будете нести приращение инкремента-декремент последовательности или что - то подобное , что в современных компиляторах. Итак, лучший способ вернуть a shared_ptr- просто вернуться по значению:

shared_ptr<T> Foo()
{
    return shared_ptr<T>(/* acquire something */);
};

Это совершенно очевидная возможность RVO для современных компиляторов C ++. Я точно знаю, что компиляторы Visual C ++ реализуют RVO, даже когда все оптимизации отключены. А с семантикой перемещения C ++ 11 эта проблема еще менее актуальна. (Но единственный способ быть уверенным - это профилировать и экспериментировать.)

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

Скажите честно: какие чувства вызывает у вас следующий код?

std::vector<std::string> get_names();
...
std::vector<std::string> const names = get_names();

Честно говоря, хотя мне следовало бы знать лучше, это меня заставляет нервничать. В принципе, при get_names() возврате мы должны скопировать a vectorof strings. Затем нам нужно скопировать его снова при инициализации names, и нам нужно уничтожить первую копию. Если stringв векторе есть N s, каждая копия может потребовать до N + 1 выделения памяти и целого ряда недружественных к кешу обращений к данным> по мере копирования содержимого строки.

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

get_names(std::vector<std::string>& out_param );
...
std::vector<std::string> names;
get_names( names );

К сожалению, этот подход далек от идеала.

  • Код вырос на 150%
  • Нам пришлось отбросить const-ness, потому что мы мутируем имена.
  • Как любят напоминать функциональные программисты, мутации делают код более сложным для размышлений, подрывая ссылочную прозрачность и уравнительные рассуждения.
  • У нас больше нет строгой семантики значений для имен.

Но действительно ли необходимо испортить наш код таким образом, чтобы повысить эффективность? К счастью, ответ оказывается отрицательным (особенно если вы используете C ++ 0x).

In silico
источник
Я не знаю, могу ли я сказать, что RVO делает вопрос спорным, поскольку возврат по ссылке решительно делает невозможным RVO.
Эдвард Стрэндж
@CrazyEddie: Верно, это одна из причин, по которой я рекомендую возвращать OP по значению.
In silico
Правило RVO, разрешенное стандартом, превосходит правила относительно отношений синхронизации / происходит раньше, гарантированные стандартом?
edA-qa mort-ora-y
1
@ edA-qa mort-ora-y: RVO явно разрешен, даже если он имеет побочные эффекты. Например, если у вас есть cout << "Hello World!";оператор в конструкторе по умолчанию и в конструкторе копирования, вы не увидите двух Hello World!s, когда действует RVO. Однако это не должно быть проблемой для правильно спроектированных интеллектуальных указателей даже при синхронизации.
In silico
23

Что касается любого умного указателя (а не только shared_ptr), я не думаю, что когда-либо приемлемо возвращать ссылку на один, и я бы очень не решался передавать их по ссылке или необработанному указателю. Зачем? Потому что вы не можете быть уверены, что позже он не будет скопирован неглубоко через ссылку. Ваш первый пункт определяет причину, по которой это должно вызывать беспокойство. Это может произойти даже в однопоточной среде. Вам не нужен одновременный доступ к данным, чтобы использовать семантику плохой копии в ваших программах. Вы действительно не контролируете, что ваши пользователи делают с указателем после того, как вы его передаете, поэтому не поощряйте злоупотребление, давая вашим пользователям API достаточно веревки, чтобы повеситься.

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

Изменить, 3 года спустя: с появлением более современных функций в C ++ я бы скорректировал свой ответ, чтобы он более приемлем для случаев, когда вы просто написали лямбда, которая никогда не живет за пределами области действия вызывающей функции и не скопировано где-то еще. Здесь, если вы хотите сэкономить очень минимальные накладные расходы на копирование общего указателя, это было бы справедливо и безопасно. Зачем? Потому что вы можете гарантировать, что ссылка никогда не будет использована неправильно.

Сан-Хасинто
источник