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

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

Ответ 1

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

Скажем, у меня есть функция 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;
}

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

Кроме того, он не может. Потому что он принимает const&.

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

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

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

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

Ответ 2

Являются ли дни прохождения const std :: string & в качестве параметра более?

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

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

Как всегда, передача константой const сохраняет много копий, когда вам не нужна копия.

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

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

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

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

Ответ 3

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

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

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

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 сек

Ответ 4

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

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

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

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


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

Ответ 5

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

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

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

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

Ответ 6

Почти.

В С++ 17 у нас есть basic_string_view<?>, basic_string_view<?> сводит нас в основном к одному узкому basic_string_view<?> использования для std::string const& parameters.

Существование хода семантики устранило один случай использования для std::string const& - если вы планируете хранение параметра, принимая std::string по значению является более оптимальным, так как вы можете move из параметра.

Если кто-то назвал вашу функцию необработанной "string" C, это означает, что когда-либо выделяется только один std::string buffer, в отличие от двух в std::string const& case.

Однако, если вы не собираетесь делать копию, взятие std::string const& все еще полезно в С++ 14.

При использовании std::string_view, если вы не передаете указанную строку API, который ожидает символьные буферы C-style '\0' -terminated, вы можете более эффективно получать функции std::string не рискуя распределением. Необработанную строку C можно даже превратить в std::string_view без какого-либо выделения или копирования символов.

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

Ответ 7

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

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

Ответ 8

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

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

#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 = С++ 11). Первое число - это число копий, а второе число - количество перемещаемых конструкций:

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

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

Ответ 9

Herb Sutter все еще записывается вместе с Bjarne 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*, как описано в Есть ли способ передать строку буквально как ссылка в С++.

(Примечание: 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

Ответ 10

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

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

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

Ответ 11

См. "Herb Sutter" Назад к основам! Essentials of Modern C++ Style ". Среди других тем он рассматривает передаваемые параметры, которые были даны в прошлом, и новые идеи, которые входят в C++ 11, и в частности рассматривает идею передачи строк по значению.

slide 24

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

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

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

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

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


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

Ответ 12

Как отмечает @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 относительно оптимальной реализации, что также является советом, данным в разговорах Herb.

Как отметил @JDługosz в комментариях, передача по значению имеет смысл только для функций, которые будут строить какой-либо объект из аргумента приемника. Когда у вас есть функция f которая копирует свой аргумент, подход с поправкой будет иметь больше накладных расходов, чем общий метод 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 и, следовательно, предотвращать (де) распределения, тогда как конструктор копирования обычно выделяет память.

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

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

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

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

Ответ 13

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

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

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

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