Покончены ли дни передачи const std::string & в качестве параметра?
Я слышал недавний доклад Герба Саттера, который утверждал, что причины передавать std::vector
и std::string
по const &
в значительной мере утратили свою актуальность. Он предложил, что написание функции вроде следующей сейчас предпочтительнее:
std::string do_something ( std::string inval )
{
std::string return_val;
// ... выполняем действия ...
return return_val;
}
Я понимаю, что return_val
будет rvalue в момент возврата из функции и, следовательно, может быть возвращен с использованием семантики перемещения, что очень эффективно. Однако inval
все еще значительно больше, чем размер ссылки (которая обычно реализуется как указатель). Это связано с тем, что std::string
имеет различные компоненты, включая указатель на кучу и член char[]
для оптимизации коротких строк. Поэтому мне кажется, что передача по ссылке все еще является хорошей идеей.
Может кто-то объяснить, почему Герб мог это сказать?
5 ответ(ов)
Нет. Многие люди воспринимают этот совет (включая Дейва Абрахамса) слишком широко, применяя его ко всем параметрам типа std::string
, что не является "лучшей практикой" для всех случаев. Передача std::string
по значению не всегда оптимальна и не подходит для любых произвольных ситуаций, так как оптимизации, о которых говорится в подобных обсуждениях и статьях, применимы только к ограниченному набору случаев.
Если вы возвращаете значение, изменяете параметр или берете значение, то передача по значению может сэкономить дорогостоящие копирования и предложить синтаксическое удобство.
Тем не менее, передача по константной ссылке экономит много копий, когда копия не требуется.
Теперь к конкретному примеру:
Однако
inval
все же значительно больше, чем размер ссылки (которая обычно реализуется как указатель). Это связано с тем, чтоstd::string
включает в себя различные компоненты, такие как указатель в куче и массивchar[]
для оптимизации коротких строк. Поэтому мне кажется, что передача по ссылке все еще является хорошей идеей. Может кто-то объяснить, почему Герб мог это сказать?
Если размер стека является проблемой (и предполагая, что это не встроенная/оптимизированная функция), return_val
+ inval
> return_val
— другими словами, пик использования стека может быть уменьшен при передаче по значению (примечание: это упрощение ABI). Тем временем, передача по константной ссылке может отключить оптимизации. Основная причина здесь не в том, чтобы избежать роста стека, а в том, чтобы гарантировать, что оптимизация может быть выполнена где это применимо.
Эпоха передачи по константной ссылке не закончилась — просто правила стали более сложными, чем когда-либо. Если производительность важна, вам будет разумно учитывать, как вы передаете такие типы, исходя из деталей, которые вы используете в своих реализациях.
Это во многом зависит от реализации компилятора.
Однако это также зависит от того, что вы используете.
Рассмотрим следующие функции:
bool foo1( const std::string v )
{
return v.empty();
}
bool foo2( const std::string & v )
{
return v.empty();
}
Эти функции реализованы в отдельном модуле компиляции, чтобы избежать инлайнинга. Тогда:
Если вы передадите литерал в обе эти функции, вы не увидите значительной разницы в производительности. В обоих случаях необходимо создать объект строки.
Если вы передадите другой объект
std::string
,foo2
будет быстрее, так какfoo1
выполнит глубокое копирование.
На моем ПК, используя g++ 4.6.1, я получил такие результаты:
- передача переменной по ссылке: 1,000,000,000 итераций → время выполнения: 2.25912 секунды
- передача переменной по значению: 1,000,000,000 итераций → время выполнения: 27.2259 секунды
- передача литерала по ссылке: 100,000,000 итераций → время выполнения: 9.10319 секунды
- передача литерала по значению: 100,000,000 итераций → время выполнения: 8.62659 секунды
Если вам действительно не нужна копия, то разумно использовать const &
. Например:
bool isprint(std::string const &s) {
return all_of(begin(s), end(s), (bool(*)(char))isprint);
}
Если вы измените этот код, чтобы принимать строку по значению, вам придется переместить или скопировать параметр, и в этом нет необходимости. Кроме того, операции копирования и перемещения могут быть более затратными по времени, а также создают новые потенциальные ситуации с ошибками; копирование или перемещение могут выбросить исключение (например, не удалось выделить память во время копирования), в то время как ссылка на существующее значение не может этого сделать.
Если вам действительно нужна копия, тогда передача и возврат по значению обычно (всегда?) являются лучшим вариантом. На самом деле, в C++03 я бы не переживал об этом, если только дополнительные копии не вызывают проблем с производительностью. Устранение копий (copy elision) кажется довольно надежным на современных компиляторах. Я думаю, что скептицизм людей и их настойчивость в необходимости проверять поддержку RVO в таблицах компиляторов в значительной степени устарели сейчас.
Вкратце, C++11 не меняет ничего в этом отношении, кроме случаев, когда люди не доверяли устранению копий.
Почти.
В C++17 у нас есть basic_string_view<?>
, что сводит применение параметров std::string const&
к очень узкому кейсу.
Существование семантики перемещения устранило один из случаев использования std::string const&
: если вы планируете сохранить параметр, передача std::string
по значению будет более оптимальной, так как вы можете move
из параметра.
Если кто-то вызовет вашу функцию с сырым C-строкой "string"
, то будет выделен только один буфер std::string
, в отличие от случая с std::string const&
, где выделяется два буфера.
Тем не менее, если вы не собираетесь делать копию, использование std::string const&
по-прежнему полезно в C++14.
С std::string_view
, пока вы не передаете эту строку в API, ожидающий C-стильные буферы с нуль-терминацией, вы можете более эффективно получить функциональность, аналогичную std::string
, без риска выделения памяти. Сырая C-строка может быть даже преобразована в std::string_view
без выделения памяти или копирования символов.
Таким образом, использование std::string const&
актуально тогда, когда вы не копируете данные полностью и собираетесь передать их в C-стиль API, который ожидает нуль-терминализованный буфер, и вам нужны более высокоуровневые строковые функции, которые предоставляет std::string
. На практике это довольно редкий набор требований.
На мой взгляд, использование ссылки на std::string
в C++ является быстрым и кратким локальным оптимизацией, в то время как передача по значению может быть (или не быть) более выгодной глобальной оптимизацией.
Таким образом, ответ таков: это зависит от обстоятельств:
- Если вы пишете весь код снаружи внутрь функций и знаете, как работает ваш код, то можно использовать ссылку
const std::string &
. - Если вы разрабатываете библиотечный код или активно используете библиотечный код, где строки часто передаются, то, вероятнее всего, вы получите больше преимущества в глобальном смысле, доверяя поведению конструктора копирования
std::string
.
Что такое лямбда-выражение и когда его следует использовать?
push_back против emplace_back: в чем разница?
Что означает T&& (двойной амперсанд) в C++11?
Какова разница между 'typedef' и 'using'?
В чем разница между constexpr и const?