Дни передачи const std :: string & в качестве параметра истекли?

604

Я слышал недавний разговор Херба Саттера, который предположил, что причины для прохождения std::vectorи std::stringпо const &большей части исчезли. Он предположил, что написание функции, такой как следующее, теперь предпочтительнее:

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

Я понимаю, что это return_valбудет r-значение в точке, которую возвращает функция, и поэтому может быть возвращено с использованием семантики перемещения, которая очень дешева. Тем не менее, invalон все еще намного больше, чем размер ссылки (которая обычно реализуется как указатель). Это связано с тем, что a std::stringимеет различные компоненты, включая указатель в кучу и элемент char[]для оптимизации коротких строк. Поэтому мне кажется, что передача по ссылке все еще хорошая идея.

Кто-нибудь может объяснить, почему Херб мог это сказать?

Benj
источник
89
Я думаю, что лучшим ответом на этот вопрос, вероятно, является чтение статьи Дэйва Абрахамса об этом на C ++. Далее . Я бы добавил, что не вижу в этом ничего такого, что квалифицируется как не по теме или не конструктивно. Это ясный вопрос о программировании, на который есть фактические ответы.
Джерри Гроб
Увлекательно, так что если вам все равно придется делать копию, передача по значению, скорее всего, быстрее, чем передача по ссылке.
Benj
3
@Sz. Я чутко отношусь к вопросам, которые ошибочно классифицируют как дубликаты и закрывают. Я не помню деталей этого дела и не пересмотрел их. Вместо этого я просто собираюсь удалить свой комментарий в предположении, что я допустил ошибку. Спасибо, что обратили на это мое внимание.
Говард
2
@HowardHinnant, большое спасибо, это всегда драгоценный момент, когда сталкиваешься с таким уровнем внимательности и чувствительности, это так освежает! (Тогда я, конечно, удалю свою.)
Sz.

Ответы:

393

Причина, по которой Херб сказал то, что он сказал, заключается в таких случаях.

Допустим, у меня есть функция, Aкоторая вызывает функцию B, которая вызывает функцию C. И Aпередает строку через Bи в C. Aне знает или не заботится C; все Aзнают о том B. То есть Cдеталь реализации B.

Допустим, что A определяется следующим образом:

void A()
{
  B("value");
}

Если B и C принимают строку const&, то это выглядит примерно так:

void B(const std::string &str)
{
  C(str);
}

void C(const std::string &str)
{
  //Do something with `str`. Does not store it.
}

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

Теперь я хочу сделать одно простое изменение: Cнужно где-то хранить строку.

void C(const std::string &str)
{
  //Do something with `str`.
  m_str = str;
}

Здравствуйте, конструктор копирования и потенциальное выделение памяти (игнорируйте Short String Optimization (SSO) ). Предполагается, что семантика перемещения в C ++ 11 позволяет удалить ненужное копирование, верно? И Aпроходит временный; нет никаких причин, почему Cнужно копировать данные. Он должен просто скрыться с тем, что ему было дано.

За исключением того, что не может. Потому что требуется const&.

Если я изменю Cсвой параметр на значение, это просто приведет Bк копированию в этот параметр; Я ничего не получаю.

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

Это дороже? Да; перемещение в значение дороже, чем использование ссылок. Это дешевле, чем копия? Не для небольших строк с SSO. Стоит ли это делать?

Это зависит от вашего варианта использования. Насколько вы ненавидите распределение памяти?

Николь Болас
источник
2
Когда вы говорите, что перемещение в значение дороже, чем использование ссылок, это все равно дороже на постоянную величину (независимо от длины перемещаемой строки), верно?
Нил Г
3
@NeilG: Вы понимаете, что означает «зависящий от реализации»? То, что вы говорите, неверно, потому что это зависит от того, реализован ли SSO и как .
ildjarn
17
@ildjarn: При анализе порядка, если наихудший случай чего-либо связан с константой, тогда это все еще постоянное время. Есть ли не самая длинная маленькая строка? Разве эта строка не требует некоторого постоянного количества времени для копирования? Разве все меньшие строки не копируют меньше времени? Затем копирование строк для маленьких строк является «постоянным временем» в анализе заказов - несмотря на то, что для маленьких строк копирование занимает разное количество времени. Анализ порядка связан с асимптотическим поведением .
Нил Г
8
@NeilG: Конечно, но ваш первоначальный вопрос был « это все еще дороже на постоянную величину (независимо от длины перемещаемой строки), верно? » Я пытаюсь подчеркнуть, что это может быть дороже с другой стороны. постоянные суммы в зависимости от длины строки, которая суммируется как «нет».
ildjarn
13
Почему строка будет movedот B до C в случае значения? Если B B(std::string b)и C, C(std::string c)то либо мы должны позвонить C(std::move(b))в B, либо bостаться неизменными (таким образом, «не двигаясь от») до выхода B. (Возможно, оптимизирующий компилятор переместит строку в соответствии с правилом «как будто», если bне используется после вызова, но я не думаю, что есть надежная гарантия.) То же самое верно для копии strto m_str. Даже если параметр функции был инициализирован с помощью значения r, он является значением l внутри функции и std::moveдолжен выходить из этого значения.
Pixelchemist
163

Дни передачи const std :: string & в качестве параметра истекли?

Нет . Многие люди принимают этот совет (в том числе Дейва Абрахамса) за пределами области, к которой он применяется, и упрощают ее для применения ко всем std::string параметрам. Всегда передача std::stringпо значению не является «наилучшей практикой» для любых и всех произвольных параметров и приложений, поскольку оптимизация этих доклады / статьи сосредоточены на применении только к ограниченному кругу дел .

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

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

Теперь к конкретному примеру:

Тем не менее ,ALAL по-прежнему намного больше, чем размер ссылки (которая обычно реализуется как указатель). Это потому, что std :: string имеет различные компоненты, включая указатель на кучу и член char [] для оптимизации коротких строк. Поэтому мне кажется, что передача по ссылке все еще хорошая идея. Кто-нибудь может объяснить, почему Херб мог это сказать?

Если размер стека является проблемой (и предполагается, что он не встроен / не оптимизирован), return_val+ inval> return_val- IOW, пиковое использование стека может быть уменьшено путем передачи значения здесь (примечание: упрощение ABI). Между тем, передача по константной ссылке может отключить оптимизацию. Основная причина здесь не в том, чтобы избежать роста стека, а в том, чтобы гарантировать, что оптимизация может быть выполнена там, где это применимо .

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

джастин
источник
3
При использовании стека типичные ABI передают одну ссылку в регистр без использования стека.
Ahcox
63

Это сильно зависит от реализации компилятора.

Однако это также зависит от того, что вы используете.

Рассмотрим следующие функции:

bool foo1( const std::string v )
{
  return v.empty();
}
bool foo2( const std::string & v )
{
  return v.empty();
}

Эти функции реализованы в отдельном модуле компиляции, чтобы избежать встраивания. Затем:
1. Если вы передадите литерал этим двум функциям, вы не увидите большой разницы в производительности. В обоих случаях необходимо создать строковый объект.
2. Если вы передадите другой объект std :: string, foo2он превзойдет foo1, потому foo1что сделает глубокое копирование.

На моем ПК, используя g ++ 4.6.1, я получил следующие результаты:

  • переменная по ссылке: 1000000000 итераций -> прошедшее время: 2,25912 сек
  • переменная по значению: 1000000000 итераций -> прошедшее время: 27,2259 с
  • литерал по ссылке: 100000000 итераций -> прошедшее время: 9.10319 сек
  • литерал по значению: 100000000 итераций -> прошедшее время: 8,62659 с
BЈовић
источник
4
Что более важно, так это то, что происходит внутри функции: нужно ли ей при вызове со ссылкой делать внутреннюю копию, которую можно пропустить при передаче по значению ?
оставил около
1
@leftaroundabout Да, конечно. Мое предположение, что обе функции делают одно и то же.
BЈовић
5
Это не моя точка зрения. Лучше передать по значению или по ссылке, зависит от того, что вы делаете внутри функции. В вашем примере вы на самом деле не используете большую часть строкового объекта, поэтому ссылка, очевидно, лучше. Но если бы задачей функции было поместить строку в некоторую структуру или выполнить, скажем, какой-то рекурсивный алгоритм, включающий несколько разбиений строки, передача по значению могла бы фактически сохранить некоторое копирование по сравнению с передачей по ссылке. Никол Болас объясняет это довольно хорошо.
оставил
3
Для меня «это зависит от того, что вы делаете внутри функции» - это плохой дизайн, поскольку вы основываете сигнатуру функции на внутренних элементах реализации.
Ганс Олссон
1
Возможно, это опечатка, но последние два буквальных тайминга имеют в 10 раз меньше циклов.
TankorSmash
54

Краткий ответ: НЕТ! Длинный ответ:

  • Если вы не будете изменять строку (обрабатывать только для чтения), передайте ее как const ref&.
    ( const ref&очевидно, что он должен оставаться в пределах видимости, в то время как выполняющая его функция)
  • Если вы планируете изменить его или знаете, что он выйдет из области видимости (потоков) , передайте его как value, не копируйте const ref&внутри тела вашей функции.

На cpp-next.com была публикация под названием «Хотите скорость, передавайте по значению!» , TL; DR:

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

ПЕРЕВОД ^

Не копировать аргументы вашей функции --- означает: если вы планируете изменить значение аргумента, скопировав его во внутреннюю переменную, просто используйте вместо него аргумент значения .

Итак, не делайте этого :

std::string function(const std::string& aString){
    auto vString(aString);
    vString.clear();
    return vString;
}

сделать это :

std::string function(std::string aString){
    aString.clear();
    return aString;
}

Когда вам нужно изменить значение аргумента в теле функции.

Вам просто нужно знать, как вы планируете использовать аргумент в теле функции. Только для чтения или НЕ ... и если он остается в рамках.

CodeAngry
источник
2
Вы рекомендуете передавать по ссылке в некоторых случаях, но вы указываете на рекомендацию, которая рекомендует всегда передавать по значению.
Кит Томпсон
3
@KeithThompson Не копируйте аргументы вашей функции. Средства не копируют во const ref&внутреннюю переменную, чтобы изменить ее. Если вам нужно изменить его ... сделайте параметр значением. Это довольно ясно для моего не говорящего по-английски я.
CodeAngry
5
@KeithThompson Цитата Руководства (не копируйте аргументы вашей функции. Вместо этого передайте их по значению и позвольте компилятору выполнить копирование.) КОПИРОВАНИЕ с этой страницы. Если это не достаточно ясно, я не могу помочь. Я не полностью доверяю компиляторам делать лучший выбор. Я предпочел бы быть очень ясным о своих намерениях в том, как я определяю аргументы функции. # 1 Если это только для чтения, это const ref&. # 2 Если мне нужно написать или я знаю, что это выходит за рамки ... Я использую значение. # 3 Если мне нужно изменить исходное значение, я прохожу мимо ref&. # 4 Я использую, pointers *если аргумент не является обязательным, поэтому я могу nullptrэто сделать.
CodeAngry
11
Я не согласен с вопросом о том, следует ли передавать по значению или по ссылке. Моя точка зрения заключается в том, что в некоторых случаях вы выступаете за передачу по ссылке, а затем цитируете (по-видимому, в поддержку своей позиции) руководство, которое рекомендует всегда передавать по значению. Если вы не согласны с указаниями, вы можете сказать об этом и объяснить, почему. (Ссылки на cpp-next.com не работают для меня.)
Кит Томпсон
4
@KeithThompson: Вы неправильно перефразируете руководство. Это не «всегда» передавать по значению. Подводя итог, можно сказать: «Если бы вы сделали локальную копию, используйте передачу по значению, чтобы компилятор выполнил эту копию за вас». В нем не сказано использовать передачу по значению, когда вы не собираетесь делать копию.
Бен Фойгт
43

Если вам на самом деле не нужна копия, ее все равно целесообразно взять const &. Например:

bool isprint(std::string const &s) {
    return all_of(begin(s),end(s),(bool(*)(char))isprint);
}

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

Если вам действительно нужна копия , то передача и возвращение по значению обычно (всегда?) Наилучший вариант. На самом деле я бы вообще не беспокоился об этом в C ++ 03, если только вы не обнаружите, что лишние копии действительно вызывают проблемы с производительностью. Копирование Elision кажется довольно надежным на современных компиляторах. Я думаю, что скептицизм и настойчивость людей в том, что вам нужно проверить таблицу поддержки компилятором RVO, в настоящее время в основном устарели.


Короче говоря, C ++ 11 на самом деле ничего не меняет в этом отношении, за исключением людей, которые не доверяют разрешению копирования.

bames53
источник
2
Конструкторы перемещения, как правило, реализуются с помощью noexcept, но конструкторы копирования, очевидно, нет.
оставил
25

Почти.

В 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, который ожидает '\0'символьные буферы, определенные в стиле C , вы можете более эффективно получать std::stringаналогичную функциональность без риска какого-либо выделения. Необработанная строка C может быть даже преобразована в std::string_viewбез выделения или копирования символов.

В этот момент, использование используется, std::string const&когда вы не копируете данные оптом и собираетесь передать их API-интерфейсу в стиле C, который ожидает нулевой завершенный буфер, и вам нужны строковые функции более высокого уровня, которые std::stringпредоставляют. На практике это редкий набор требований.

Якк - Адам Невраумонт
источник
2
Я ценю этот ответ, но хочу отметить, что он страдает (как и многие качественные ответы) от некоторого смещения, специфичного для конкретной области. То есть: «На практике это редкий набор требований»… в моем собственном опыте разработки эти ограничения, которые кажутся автору необычно узкими, встречаются буквально все время. На это стоит обратить внимание.
fish2000
1
@ fish2000 Для ясности, std::stringчтобы доминировать, вам нужны не только некоторые из этих требований, но и все они. Я признаю, что любой из них или даже два из них являются общими. Может быть, вам обычно нужны все 3 (например, вы делаете какой-то анализ строкового аргумента, чтобы выбрать, на какой C API вы собираетесь передавать его оптом?)
Якк - Адам Невраумонт
@ Yakk-AdamNevraumont Это вещь YMMV - но это частый случай использования, если (скажем), вы программируете с использованием POSIX или других API, где семантика C-строки, например, является наименьшим общим знаменателем. Я должен сказать, что мне действительно нравится std::string_view- как вы отмечаете , «необработанную строку C можно даже превратить в std :: string_view без какого-либо выделения или копирования символов», что стоит запомнить тем, кто использует C ++ в контексте такое использование API, действительно.
fish2000
1
@ fish2000 "'Необработанную строку C можно даже превратить в std :: string_view без какого-либо выделения или копирования символов', о чем стоит помнить". Действительно, но это не учитывает большую часть - в случае, если необработанная строка является строковым литералом, она даже не требует выполнения strlen () !
Дон Хэтч
17

std::stringэто не простые старые данные (POD) , и их необработанный размер - не самая актуальная вещь. Например, если вы передадите строку, длина которой превышает длину SSO и выделена в куче, я ожидаю, что конструктор копирования не будет копировать хранилище SSO.

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

щенок
источник
2
Интересный момент в том, что конструктор копирования достаточно умен, чтобы не беспокоиться о едином входе, если он его не использует. Наверное, правильно, мне придется проверить, правда ли это ;-)
Benj
3
@Benj: Старый комментарий, я знаю, но если SSO достаточно мал, его копирование безусловно выполняется быстрее, чем создание условного перехода. Например, 64 байта являются строкой кэша и могут быть скопированы за очень тривиальное время. Вероятно, 8 циклов или меньше на x86_64.
Zan Lynx
Даже если единый вход не копируется конструктором копирования, std::string<>из стека выделяется 32 байта, 16 из которых необходимо инициализировать. Сравните это только с 8 байтами, выделенными и инициализированными для ссылки: это вдвое больше объема работы ЦП и занимает в четыре раза больше места в кеше, которое не будет доступно для других данных.
cmaster - восстановить монику
О, и я забыл говорить о передаче аргументов функции в регистрах; это привело бы к использованию стека ссылки на ноль для последнего вызываемого ...
cmaster - восстановить монику
16

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

Вот код для измерения того, что спрашивают:

#include <iostream>

struct string
{
    string() {}
    string(const string&) {std::cout << "string(const string&)\n";}
    string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
#if (__has_feature(cxx_rvalue_references))
    string(string&&) {std::cout << "string(string&&)\n";}
    string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
#endif

};

#if PROCESS == 1

string
do_something(string inval)
{
    // do stuff
    return inval;
}

#elif PROCESS == 2

string
do_something(const string& inval)
{
    string return_val = inval;
    // do stuff
    return return_val; 
}

#if (__has_feature(cxx_rvalue_references))

string
do_something(string&& inval)
{
    // do stuff
    return std::move(inval);
}

#endif

#endif

string source() {return string();}

int main()
{
    std::cout << "do_something with lvalue:\n\n";
    string x;
    string t = do_something(x);
#if (__has_feature(cxx_rvalue_references))
    std::cout << "\ndo_something with xvalue:\n\n";
    string u = do_something(std::move(x));
#endif
    std::cout << "\ndo_something with prvalue:\n\n";
    string v = do_something(source());
}

Для меня это выводы:

$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
$ a.out
do_something with lvalue:

string(const string&)
string(string&&)

do_something with xvalue:

string(string&&)
string(string&&)

do_something with prvalue:

string(string&&)
$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
$ a.out
do_something with lvalue:

string(const string&)

do_something with xvalue:

string(string&&)

do_something with prvalue:

string(string&&)

В таблице ниже приведены мои результаты (используется clang -std = c ++ 11). Первое число - это число конструкций копирования, а второе - количество конструкций перемещения:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+

Решение для передачи по значению требует только одной перегрузки, но требует дополнительной конструкции перемещения при передаче значений l и xvalue. Это может или не может быть приемлемым для любой конкретной ситуации. Оба решения имеют свои преимущества и недостатки.

Говард Хиннант
источник
1
std :: string - это стандартный библиотечный класс. Это уже и подвижный и копируемый. Я не понимаю, насколько это актуально. ОП спрашивает больше о производительности перемещения и ссылок , а не о производительности перемещения и копирования.
Николь Болас
3
Этот ответ подсчитывает количество ходов и копирует, что std :: string будет проходить в соответствии с схемой передачи по значению, описанной как Хербом, так и Дейвом, по сравнению с передачей по ссылке с парой перегруженных функций. Я использую код OP в демо, за исключением замены в фиктивной строке, чтобы выкрикивать, когда она копируется / перемещается.
Говард Хиннант,
Вероятно, вам следует оптимизировать код перед выполнением тестов ...
Парамагнитный круассан
3
@TheParamagnCroissant: Вы получили разные результаты? Если да, то какой компилятор с какими аргументами командной строки?
Говард Хиннант
14

Херб Саттер до сих пор записывается вместе с Бьярном Страуструпом в const std::string&качестве параметра параметра; видеть https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in .

Здесь есть подводный камень, не упомянутый ни в одном другом ответе: если вы передаете строковый литерал const std::string&параметру, он передаст ссылку на временную строку, созданную на лету для хранения символов литерала. Если вы затем сохраните эту ссылку, она будет недействительной после освобождения временной строки. Чтобы быть в безопасности, вы должны сохранить копию , а не ссылку. Проблема связана с тем, что строковые литералы являются const char[N]типами, требующими продвижения std::string.

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

(Примечание: Sutter & Stroustroup советуют, если вы сохраняете копию строки, также предоставьте перегруженную функцию с параметром && и std :: move () it.)

#include <string>
#include <iostream>
class WidgetBadRef {
public:
    WidgetBadRef(const std::string& s) : myStrRef(s)  // copy the reference...
    {}

    const std::string& myStrRef;    // might be a reference to a temporary (oops!)
};

class WidgetSafeCopy {
public:
    WidgetSafeCopy(const std::string& s) : myStrCopy(s)
            // constructor for string references; copy the string
    {std::cout << "const std::string& constructor\n";}

    WidgetSafeCopy(const char* cs) : myStrCopy(cs)
            // constructor for string literals (and char arrays);
            // for minor efficiency only;
            // create the std::string directly from the chars
    {std::cout << "const char * constructor\n";}

    const std::string myStrCopy;    // save a copy, not a reference!
};

int main() {
    WidgetBadRef w1("First string");
    WidgetSafeCopy w2("Second string"); // uses the const char* constructor, no temp string
    WidgetSafeCopy w3(w2.myStrCopy);    // uses the String reference constructor
    std::cout << w1.myStrRef << "\n";   // garbage out
    std::cout << w2.myStrCopy << "\n";  // OK
    std::cout << w3.myStrCopy << "\n";  // OK
}

ВЫВОД:

const char * constructor
const std::string& constructor

Second string
Second string
circlepi314
источник
Это другая проблема, и WidgetBadRef не должен иметь const & параметр, чтобы пойти не так. Вопрос в том, что если WidgetSafeCopy просто принимает строковый параметр, будет ли он медленнее? (Я думаю, что копию, временную для участника, определенно легче обнаружить)
Superfly Jon
Обратите внимание, что T&&это не всегда универсальная ссылка; на самом деле, std::string&&это всегда будет ссылка rvalue, а не универсальная ссылка, потому что никакого вывода типа не производится . Таким образом, совет Stroustroup & Sutter не противоречит Мейерсу.
Джастин Тайм - Восстановить Монику
@JustinTime: спасибо; Я удалил неверное последнее предложение, утверждая, что фактически std :: string && будет универсальной ссылкой.
circlepi314
@ circlepi314 Добро пожаловать. Это легко сделать путаницей, иногда это может сбивать с толку, T&&является ли какая-либо конкретная ссылка выведенной или не выведенной ссылкой; возможно, было бы яснее, если бы они вводили другой символ для универсальных ссылок (например &&&, в виде комбинации &и &&), но это, вероятно, выглядело бы глупо.
Джастин Тайм - Восстановить Монику
8

ИМО, используя ссылку C ++ для std::string - это быстрая и короткая локальная оптимизация, в то время как передача по значению может быть (или нет) лучшей глобальной оптимизацией.

Таким образом, ответ: это зависит от обстоятельств:

  1. Если вы пишете весь код извне для внутренних функций, вы знаете, что делает код, вы можете использовать ссылку const std::string &.
  2. Если вы пишете код библиотеки или интенсивно используете код библиотеки для передачи строк, вы, скорее всего, получите больше в глобальном смысле, доверяя std::stringповедению конструктора копирования.
digital_infinity
источник
6

См. «Херб Саттер» «Назад к основам! Основы современного стиля C ++» . Среди других тем он рассматривает рекомендации по передаче параметров, которые были даны в прошлом, а также новые идеи, которые приходят с C ++ 11, и, в частности, рассматривает Идея передачи строк по значению.

слайд 24

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

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

См. Его слайд 27: для «установленных» функций опция 1 такая же, как и всегда. Вариант 2 добавляет перегрузку для ссылки на значение, но это дает комбинаторный взрыв, если имеется несколько параметров.

Только для параметров «приемника», где должна быть создана строка (без изменения ее существующего значения), трюк передачи по значению действителен. То есть конструкторы в которых параметр непосредственно инициализирует член соответствующего типа.

Если вы хотите увидеть, насколько глубоко вы можете беспокоиться об этом, посмотрите презентацию Николая Йосуттиса и удачи с этим ( «Отлично - Готово!» N раз после обнаружения ошибки в предыдущей версии. Вы когда-нибудь были?)


Это также обобщено как ⧺F.15 в Стандартных руководящих принципах.

JDługosz
источник
3

Как отмечает @ JDługosz в комментариях, Херб дает другой совет в другом (позже?) Выступлении, смотрите примерно здесь: https://youtu.be/xnqTKD8uD64?t=54m50s .

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

Этот общий подход добавляет только издержки конструктора перемещения для аргументов lvalue и rvalue по сравнению с оптимальной реализацией fспециально настроенных аргументов lvalue и rvalue соответственно. Чтобы понять, почему это так, предположим, что fпринимает параметр-значение, где Tесть некоторый тип копирования и перемещения:

void f(T x) {
  T y{std::move(x)};
}

Вызов fс аргументом lvalue приведет к тому, что конструктор копирования вызывается для конструирования x, а конструктор перемещения вызывается для конструирования y. С другой стороны, вызов fс аргументом rvalue вызовет конструктор перемещения для вызова и вызовет конструктор xдругого перемещения y.

В общем, оптимальная реализация fаргументов для lvalue выглядит следующим образом:

void f(const T& x) {
  T y{x};
}

В этом случае для конструирования вызывается только один конструктор копирования y. Оптимальная реализация fаргументов для rvalue, опять же, в общем, выглядит следующим образом:

void f(T&& x) {
  T y{std::move(x)};
}

В этом случае для конструирования вызывается только один конструктор перемещения y.

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

Как отметил @ JDługosz в комментариях, передача по значению имеет смысл только для функций, которые будут создавать некоторый объект из аргумента приемника. Когда у вас есть функция, fкоторая копирует свой аргумент, подход с передачей по значению будет иметь больше издержек, чем общий подход с передачей по константной ссылке. Подход с передачей по значению для функции, fкоторая сохраняет копию своего параметра, будет иметь вид:

void f(T x) {
  T y{...};
  ...
  y = std::move(x);
}

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

void f(const T& x) {
  T y{...};
  ...
  y = x;
}

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

Для аргумента rvalue наиболее оптимальная реализация, в fкоторой сохраняется копия, имеет вид:

void f(T&& x) {
  T y{...};
  ...
  y = std::move(x);
}

Таким образом, в этом случае только назначение перемещения. Передача r-значения в версию, fкоторая принимает константную ссылку, стоит только назначение, а не перемещение. Итак, условно говоря, версияf использования константной ссылки в этом случае в качестве общей реализации является предпочтительным.

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

Тон ван ден Хеувел
источник
Посмотрите на выводы Херба Саттера в моем новом ответе: делайте это только тогда, когда вы перемещаете конструкцию , а не перемещаете назначение.
JDługosz
1
@ JDługosz, спасибо за указатель на выступление Херба, я просто посмотрел его и полностью пересмотрел свой ответ. Я не знал о (переместить) назначить совет.
Тон ван ден Хеувел
эта цифра и рекомендации находятся сейчас в документе Стандартных руководящих принципов .
JDługosz
1

Проблема в том, что «const» - это не гранулярный классификатор. Что обычно подразумевается под «const string ref», так это «не изменяйте эту строку», а не «не изменяйте счетчик ссылок». В C ++ просто нет возможности сказать, какие члены являются «const». Они либо все, либо ни один из них.

Чтобы обойти эту проблему языка, STL мог бы разрешить "C ()" в вашем примере в любом случае сделать семантическую копию перемещения и покорно игнорировать "const" в отношении счетчика ссылок (изменяемых). Пока это было точно указано, это было бы хорошо.

Так как STL этого не делает, у меня есть версия строки, которая const_casts <> удаляет счетчик ссылок (нет способа задним числом сделать что-то изменяемое в иерархии классов), и - о чудо - вы можете свободно передавать cmstring как константные ссылки, и делайте их копии в глубоких функциях, весь день, без утечек или проблем.

Поскольку C ++ здесь не предлагает «гранулярность const производного класса», написание хорошей спецификации и создание блестящего нового объекта «const movable string» (cmstring) - лучшее решение, которое я видел.

Эрик Аронесты
источник
@BenVoigt да ... вместо отбрасывания он должен быть изменяемым ... но вы не можете изменить член STL на изменяемый в производном классе.
Эрик Аронесты