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

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

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

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

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

13 ответов


причина, по которой Херб сказал то, что он сказал, Из-за подобных случаев.

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

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

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

если B и С Берем строку 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;
}

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

вот только не может. Потому что для этого требуется const&.

если я изменю C чтобы взять его параметр по значению, это просто вызывает B для копирования в этот параметр; I получить ничего.

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

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

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


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

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

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

как всегда, прохождение ссылки const экономит много копирования если вам не нужна копия.

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

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

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

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


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

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

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

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

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

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

  • переменная по ссылке: 1000000000 итераций -> прошедшее время: 2.25912 сек
  • переменная по значению: 1000000000 итераций -> прошедшее время: 27.2259 сек
  • литерал по ссылке: 100000000 итераций -> прошедшее время: 9.10319 сек
  • литерал по значению: 100000000 итераций - > время истекло: 8.62659 сек

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

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

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

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


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


короткий ответ: нет! ответ:

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

там был пост на cpp-next.com под названием " хотите скорость, пройти по значению!". TL; DR:

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

перевод ^

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

и Не делай этого:

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

этого:

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

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

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


почти.

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

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


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

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


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

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

#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   |
+----+--------+--------+---------+

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


герб Саттер все еще находится на записи, вместе с Бьярне Stroustroup, рекомендуя 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 ().)

#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

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

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

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

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

slide 24

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

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

см. его слайд 27: для функций" set " Вариант 1 такой же, как и всегда. Вариант 2 добавляет перегрузку для ссылки rvalue, но это дает комбинаторный взрыв, если есть несколько параметров.

это только для параметров "sink", где должна быть создана строка (не иметь ее существующее значение изменено), что трюк pass-by-value действителен. То есть, конструкторы в котором параметр непосредственно инициализирует элемент соответствующего типа.

Если вы хотите увидеть, как глубоко вы можете беспокоиться об этом, смотрите Николай Джосутису это презентация и удачи с этим ("Идеальное - Сделано!" N раз после обнаружения ошибки в предыдущей версии. Был там когда-нибудь?)


это тоже резюмируется как ⧺Ф. 15 в стандартных принципов.


как указывает @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 будет вызовите конструктор move для построения x, и еще один конструктор перемещения, который будет вызван для построения y.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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