c++ - стандарт - что такое c>++ 11




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

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

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


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

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

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

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

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

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

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

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


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

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

Поэтому ответ: это зависит от обстоятельств:

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

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


Почти.

В 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 . На практике это редкий набор требований.


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

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

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


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

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

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

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

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

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

#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 ​​и x. Это может быть или не быть приемлемым для любой конкретной ситуации. Оба решения имеют свои преимущества и недостатки.


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

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

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

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





c++11