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

Я слышал недавний разговор Херба Саттера, который предположил, что причины пройти 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 все еще намного больше, чем размер ссылки (которая обычно реализуется как указатель). Это потому что std::string имеет различные компоненты, включая указатель в кучу и член char[] для оптимизации коротких строк. Поэтому мне кажется, что передача по ссылке все еще хорошая идея.

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

13 ответов

Решение

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

Допустим, у меня есть функция 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. Стоит ли это делать?

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

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

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

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

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

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

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

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

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

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

  • Если вы не будете изменять строку (обрабатывать только для чтения), передайте ее как 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;
}

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

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

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

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

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

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 с

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

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

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

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

На этом этапе использование для std::string const& это когда вы не копируете данные оптом и собираетесь передать их в C-стиль API, который ожидает нулевой завершенный буфер, и вам нужны строковые функции более высокого уровня, которые 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. Это может или не может быть приемлемым для любой конкретной ситуации. Оба решения имеют свои преимущества и недостатки.

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

слайд 24

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

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

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

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

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


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

Херб Саттер до сих пор записывается вместе с Бьярном Страуструпом в рекомендации 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

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

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

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

Как отмечает @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);
}

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

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

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

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

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

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

Другие вопросы по тегам