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




что такое c>++ 11 (11)

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

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


Почти.

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

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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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


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

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

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* , как описано в разделе «Есть ли способ передать строковый литерал как ссылку в 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

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

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

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

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

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

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

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

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


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

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


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

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

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

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


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

Предположим, у меня есть функция 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) ). Предполагается, что семантика перемещения C ++ 11 позволяет удалить ненужное копирование, не так ли? А A проходит временное; нет причин, по которым C должен был бы скопировать данные. Он должен просто скрыться от того, что ему было дано.

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

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

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

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

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





c++11