Чи закінчуються дні проходження const std :: string & як параметр?


604

Я чув недавню розмову з Herb Sutter , який припустив , що причини , щоб пройти std::vectorі std::stringпо const &в значній мірі зникли. Він припустив, що зараз бажано написати таку функцію, як наступна:

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

Я розумію, що в return_valточці функція буде ревальвацією, і функція повертається, і тому її можна повернути за допомогою семантики переміщення, яка є дуже дешевою. Однак, invalвін все ще набагато більший, ніж розмір посилання (який зазвичай реалізується як вказівник). Це тому, що у A std::stringє різні компоненти, включаючи вказівник на купу та член char[]для оптимізації коротких рядків. Тож мені здається, що проходження посилання все ще є хорошою ідеєю.

Хтось може пояснити, чому Герб могла сказати це?


89
Я думаю, що найкраща відповідь на питання - це, мабуть, прочитати статтю Дейва Абрахамса про це на C ++ Next . Додам, що я нічого не бачу в цьому, що могло б вважатись поза темою чи не конструктивною. Це чітке питання, щодо програмування, на яке є фактичні відповіді.
Джеррі Труну

Захоплююче, тому, якщо вам доведеться все-таки зробити копію, передача за значенням, швидше за все, швидша, ніж пропуск посилання.
Бендж

3
@Sz. Я чутливий до того, що питання, помилково класифіковані як дублікати та закриті. Я не пам’ятаю подробиць цієї справи і не переглядав їх повторно. Натомість я просто збираюся видалити свій коментар з припущення, що я допустив помилку. Дякую, що ви звернули на це увагу.
Говард Хінант

2
@HowardHinnant, велике спасибі, це завжди дорогоцінний момент, коли хтось стикається з цим рівнем уважності та чуйності, це так освіжає! (Тоді я, звичайно,
видалю

Відповіді:


393

Причина Герб сказала те, що він сказав, через такі випадки.

Скажімо, у мене є функція, 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слід було б копіювати дані. Він повинен просто уникнути того, що йому було дано.

За винятком цього не може. Тому що це займає a const&.

Якщо я зміню Cприйняти його параметр за значенням, це просто змушує Bзробити копію в цей параметр; Я нічого не отримую.

Тож якби я щойно передавав strзначення за всіма функціями, покладаючись на std::moveпереміщення даних навколо, у нас не було б цієї проблеми. Якщо хтось хоче дотримуватися цього, він може. Якщо вони цього не роблять.

Це дорожче? Так; перехід до значення дорожче, ніж використання посилань. Це дешевше, ніж копія? Не для невеликих струн із SSO. Чи варто це робити?

Це залежить від вашого випадку використання. Скільки ви ненавидите виділення пам'яті?


2
Коли ви говорите, що переміщення у значення дорожче, ніж використання посилань, це все-таки дорожче постійної суми (незалежно від довжини рядка, що переміщується) так?
Ніл G

3
@NeilG: Ви розумієте, що означає "залежність від реалізації"? Те, що ви говорите, невірно, оскільки це залежить від того, як і як реалізується SSO.
ildjarn

17
@ildjarn: Якщо аналіз порядку, якщо найгірший випадок чогось пов'язаний постійною, то це все-таки постійний час. Чи немає найдовшої невеликої струни? Невже цей рядок не потребує певної кількості часу для копіювання? Чи не всі менші рядки копіюють менше часу? Тоді копіювання рядків для невеликих рядків є "постійним часом" для аналізу порядку - незважаючи на невеликі рядки, які потребують копіювання різної кількості часу. Аналіз замовлень стосується асимптотичної поведінки .
Ніл Г

8
@NeilG: Звичайно, але ваш оригінальний питання « що по - , як і раніше дорожче на суму постійної (незалежно від довжини рядка переміщається) правильно? » Справа в тому, що я намагаюся зробити це, це може бути дорожче за різному постійні суми залежно від довжини рядка, яка підсумовується як "ні".
ildjarn

13
Чому рядок буде movedвід B до C у випадку значення за значенням? Якщо B є, B(std::string b)а C є, C(std::string c)то ми або мусимо зателефонувати C(std::move(b))в B, або bтреба залишатися незмінним (таким чином, "не вилучений з") до виходу B. (Можливо, що оптимізує компілятор переміщує рядок під неначебто правило , якщо bне використовується після виклику , але я не думаю , що є сильна гарантія.) Те ж саме вірно і для копії strз m_str. Навіть якщо параметр функції був ініціалізований з rvalue, він є значенням всередині функції і std::moveпотрібно перейти від цього значення.
Піксельхіміст

163

Чи закінчуються дні проходження const std :: string & як параметр?

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

Якщо ви повертаєте значення, мутуєте параметр або приймаєте значення, то передача за значенням може заощадити дороге копіювання та запропонувати синтаксичну зручність.

Як ніколи, проходження повз посилання const економить багато копіювання, коли копія вам не потрібна .

Тепер до конкретного прикладу:

Однак inval все ще набагато більший, ніж розмір посилання (який зазвичай реалізується як покажчик). Це тому, що std :: string має різні компоненти, включаючи вказівник на купу та char-член [] для оптимізації коротких рядків. Тож мені здається, що проходження посилання все ще є хорошою ідеєю. Хтось може пояснити, чому Герб могла сказати це?

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

Дні, що проходять повз посилання const, не закінчуються - правила просто складніші, ніж були раніше. Якщо ефективність важлива, вам буде розумно розглянути спосіб передачі цих типів, виходячи з деталей, які ви використовуєте у своїх реалізаціях.


3
При використанні стеку типові ABI передають єдину посилання в регістр без використання стека.
ахкокс

63

Це дуже залежить від реалізації компілятора.

Однак це також залежить від того, що ви використовуєте.

Розглянемо наступні функції:

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
Що важливіше - це те, що відбувається всередині функції: чи потрібно, якщо вона викликається з посиланням, зробити внутрішню копію, яку можна опустити при переході за значенням ?
близько

1
@leftaroundabout Так, поза сумнівом. Моє припущення, що обидві функції роблять абсолютно те саме.
BЈович

5
Це не моя суть. Будь кращий прохід за значенням або посиланням залежить від того, що ви робите всередині функції. У вашому прикладі ви фактично не використовуєте велику частину об'єкта рядка, тому посилання, очевидно, краще. Але якщо завданням функції було розмістити рядок у якійсь структурі або виконати, скажімо, якийсь рекурсивний алгоритм, що включає декілька розщеплень рядка, передаючи за значенням, насправді може зберегти деяке копіювання, порівняно з передачею посилання. Нікол Болас це досить добре пояснює.
Ліворуч близько

3
Для мене "це залежить від того, що ви робите всередині функції", це поганий дизайн - оскільки ви базуєте підпис функції на внутрішніх умовах реалізації.
Ганс Олссон

1
Це може бути помилка друку, але останні два буквальні таймінги мають на 10 разів менше циклів.
TankorSmash

54

Коротка відповідь: НІ! Довга відповідь:

  • Якщо ви не зміните рядок (трактувати як лише для читання), передайте його як 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;
}

Коли вам потрібно змінити значення аргументу у вашому функції функції.

Вам просто потрібно знати, як ви плануєте використовувати аргумент в тілі функції. Тільки для читання або НЕ ... і якщо це дотримується рамки.


2
Ви рекомендуєте пройти посилання в деяких випадках, але ви вказуєте на керівний принцип, який рекомендує завжди проходити за значенням.
Кіт Томпсон

3
@KeithThompson Не копіюйте свої аргументи функції. Означає, що не скопіюйте на const ref&внутрішню змінну, щоб змінити її. Якщо вам потрібно змінити ... зробіть параметр значенням. Це досить зрозуміло для моєї не-англійської мови, що розмовляє.
CodeAngry

5
@KeithThompson Керівництво цитата (не копіюйте ваші аргументи функції. Замість цього, передавати їх за значенням , і нехай компілятор робити копіювання.) Скопійована з цієї сторінки. Якщо це недостатньо ясно, я не можу допомогти. Я не повністю довіряю компіляторам, щоб зробити найкращий вибір. Я б краще зрозумів свої наміри в тому, як я визначаю аргументи функції. # 1 Якщо це лише для читання, це a const ref&. # 2 Якщо мені потрібно написати це або я знаю, що він виходить за межі ... Я використовую значення. # 3 Якщо мені потрібно змінити початкове значення, я проходжу повз ref&. # 4 Я використовую, pointers *якщо аргумент необов’язковий, щоб я міг nullptrйого.
CodeAngry

11
Я не ставлю сторону в питанні про те, чи потрібно переходити за значенням чи за посиланням. Моя думка полягає в тому, що ви виступаєте за проходження посилань у деяких випадках, але потім цитуєте (здавалося б, підтримку вашої позиції) настанову, яка рекомендує завжди проходити цінність. Якщо ви не згодні з настановою, ви можете сказати так і пояснити, чому. (Посилання на cpp-next.com не працюють для мене.)
Кіт Томпсон,

4
@KeithThompson: Ти неправильно перефразовуєш керівництво. Це не завжди "переходити" за значенням. Підсумовуючи це, "Якщо ви зробили локальну копію, використовуйте pass by value, щоб компілятор виконав цю копію для вас." Це не означає використовувати пропускну вартість, коли ви не збиралися робити копію.
Ben Voigt

43

Якщо вам справді не потрібна копія, її все-таки розумно взяти const &. Наприклад:

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

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

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


Коротше кажучи, C ++ 11 насправді нічого не змінює в цьому плані, крім людей, які не довіряли копіюванню elision.


2
Конструктори переміщення зазвичай реалізуються за допомогою noexcept, але конструктори копій, очевидно, ні.
Ліворуч близько

25

Майже.

У 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, який очікує, що '\0'буфери символів, визначені у стилі C , ви можете більш ефективно отримати std::stringфункціональність, не ризикуючи жодним розподілом. Сировинний рядок C навіть може бути перетворений у std::string_viewбез будь-якого виділення чи копіювання символів.

У цей момент використання використовується std::string const&, коли ви не копіюєте дані оптом, і збираєтесь передати їх API API стилю, який очікує нульового завершеного буфера, і вам потрібні строкові функції вищого рівня, які std::stringпередбачено. На практиці це рідкісний набір вимог.


2
Я високо ціную цю відповідь, але хочу зазначити, що вона потерпає (як і багато якісних відповідей) від невеликого зміщення, що стосується домену. До речі: "На практиці це рідкісний набір вимог" ... в моєму власному досвіді розробки ці обмеження - які видаються автором аномально вузькими - дотримуються, начебто, буквально весь час. Варто вказати на це.
fish2000

1
@ fish2000 Щоб бути зрозумілим, для std::stringдомінування вам не потрібні лише деякі з цих вимог, але всі вони. Будь-який з них, а то й два з них, я, визнаю, є загальним. Можливо, вам зазвичай потрібні всі 3 (наприклад, ви робите певний аналіз строкового аргументу, щоб вибрати, до якого API API ви збираєтеся передати його оптом?)
Yakk - Adam Nevraumont

@ Yakk-AdamNevraumont Це річ YMMV - але це часті випадки використання, якщо (скажімо, ви програмуєте проти POSIX або інших API, де семантика C-рядків є, наприклад, найнижчим загальним знаменником. Я мушу сказати дійсно, що мені подобається std::string_view- як ви зазначаєте , "необроблений рядок C можна навіть перетворити на std :: string_view без будь-якого виділення чи копіювання символів", що варто пам’ятати тим, хто використовує C ++ у контексті подібне використання API.
fish2000

1
@ fish2000 "" Сирий рядок C можна навіть перетворити на std :: string_view без будь-якого виділення чи копіювання символів ", що варто щось запам'ятати". Дійсно, але це залишає найкращу частину - у випадку, якщо необроблена рядок є рядковою літеральною, вона навіть не вимагає строку виконання () !
Дон Хетч

17

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

Причина, яку це рекомендується, полягає в тому, що invalвона будується з виразу аргументу, і, таким чином, завжди переміщується або копіюється у відповідних випадках - втрати продуктивності немає, якщо припустити, що вам потрібно володіти аргументом. Якщо цього не зробити, constпосилання все ж може бути кращим способом.


2
Цікавий момент, коли конструктор копій досить розумний, щоб не турбуватися про SSO, якщо він не використовує його. Можливо, правильно, мені доведеться перевірити, чи це правда ;-)
Бендж,

3
@Benj: Старий коментар я знаю, але якщо SSO досить малий, копіюючи його беззастережно, це швидше, ніж робити умовну гілку. Наприклад, 64 байти - це лінія кешу, яку можна скопіювати за дійсно тривіальну кількість часу. Ймовірно, 8 циклів або менше на x86_64.
Zan Lynx

Навіть якщо SSO не скопійовано конструктором копій, а std::string<>- 32 байти, виділених із стека, 16 з яких потрібно ініціалізувати. Порівняйте це з лише 8 байтами, виділеними та ініціалізованими для довідки: це вдвічі більше роботи CPU, і це займає в чотири рази більше місця кешу, яке не буде доступно для інших даних.
cmaster - відновити моніку

О, і я забув поговорити про передачу аргументів функції в регістри; це призведе до того, що використання останнього
запиту

16

Я скопіював / вставив відповідь з цього питання тут і змінив назви та написання, щоб відповідати цьому питанню.

Ось код для вимірювання запитуваного:

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

Рішення прохідної вартості вимагає лише однієї перевантаження, але коштує додаткової конструкції руху при передачі значень і значень xvalues. Це може бути або не бути прийнятним для будь-якої ситуації. Обидва рішення мають переваги та недоліки.


1
std :: string - це стандартний клас бібліотеки. Це вже є рухомим та копіюваним. Я не бачу, наскільки це актуально. ОП запитує більше про ефективність переміщення по відношенню до посилань , а не про те, щоб рухатись проти копії.
Нікол Болас

3
Ця відповідь підраховує кількість переходів та копіювання рядка std :: буде піддано проектуванні прохідної вартості, описаному Herb та Dave, проти передачі посилання з парою перевантажених функцій. Я використовую код OP в демонстраційній версії, за винятком заміни в макетній рядку, щоб викрикувати, коли він копіюється / переміщується.
Говард Хінант

Ви, ймовірно, повинні оптимізувати код перед тим, як проводити тести…
Парамагнітний круасан

3
@TheParamagneticCroissant: Чи отримали ви різні результати? Якщо так, використовуючи який компілятор з якими аргументами командного рядка?
Говард Хінант

14

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

Це інша проблема, і для WidgetBadRef не потрібно мати const & параметр, щоб піти не так. Питання в тому, якби WidgetSafeCopy просто взяв рядовий параметр, чи буде це повільніше? (Я думаю, що тимчасову копію для учасника, безумовно, простіше помітити)
Superfly Jon

Зауважте, що T&&не завжди універсальна довідка; насправді, std::string&&завжди буде посиланням на реальне значення, а ніколи не є універсальним посиланням, оскільки не проводиться дедукція типу . Таким чином, поради Stroustroup & Sutter не суперечать Meyers '.
Час Джастіна - Відновіть Моніку

@JustinTime: дякую; Я видалив неправильне остаточне речення, стверджуючи, що фактично std :: string && буде універсальним посиланням.
кругпі314

@ circlepi314 Запрошуємо вас Це легко змішувати, іноді може бути заплутано, чи будь-яка дана T&&є виведеною універсальною посиланням або невиведеною посиланням на оцінку; це, мабуть, було б зрозуміліше, якби вони ввели інший символ для універсальних посилань (наприклад &&&, як комбінація &та &&), але це, мабуть, виглядало б просто нерозумно.
Час Джастіна - Відновити Моніку

8

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

Отже, відповідь така: це залежить від обставин:

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

6

Див. "Herb Sutter" Назад до основ! Основи сучасного стилю C ++ " . Серед інших тем він розглядає поради щодо передачі параметрів, які були наведені в минулому, та нові ідеї, що надходять із C ++ 11, і конкретно розглядає ідея передачі рядків за значенням.

слайд 24

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

Це тому, що ви змушуєте його завжди робити повну копію (а потім переміщуватися на місце), тоді як const&версія оновить стару рядок, яка може повторно використовувати вже виділений буфер.

Дивіться його слайд 27: Для "встановлених" функцій варіант 1 такий же, як і раніше. Варіант 2 додає перевантаження для посилання на rvalue, але це дає комбінаторний вибух, якщо є кілька параметрів.

Трюк пропускання значення справедливий лише для параметрів “потоплення”, де повинна бути створена рядок (не змінювати існуюче значення). Тобто конструктори, у яких параметр безпосередньо ініціалізує член відповідного типу.

Якщо ви хочете побачити, наскільки глибоко ви можете переживати з цього приводу, подивіться презентацію Ніколая Йосуттіса і удачі в цьому ( "Ідеально - Готово!" П. Разів після того, як виникла помилка з попередньою версією. Коли-небудь там було?)


Це також узагальнено як ⧺F.15 у Стандартних інструкціях.


3

Як @ 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яка копіює її аргумент, підхід "передача за значенням" матиме більше накладних витрат, ніж загальний підхід "по базі". Підхід передачі значення для функції, 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% правильно (див. Розмову про деталі горі).


Дивіться висновки Герб Саттер у моїй новій відповіді: робіть це лише тоді, коли ви переміщуєте конструкцію , а не переміщуйте призначення.
JDługosz

1
@ JDługosz, дякую за вказівник до розмови Герба, я просто спостерігав за цим і повністю переглянув свою відповідь. Мені не було відомо про (хід) призначення поради.
Тон ван ден Хевель

ця цифра та поради є зараз у Стандартних керівних документах .
JDługosz

1

Проблема полягає в тому, що "const" є незернистим класифікатором. Що зазвичай означає "const string ref", це "не змінювати цей рядок", не "не змінювати кількість посилань". Просто в C ++ немає способу сказати, хто з членів "const". Вони або всі є, або ніхто з них не є.

Для того, щоб зламати цю проблему з мовою, STL може дозволити "C ()" у вашому прикладі зробити хоч семантичну копію в будь-якому випадку , а також гідно ігнорувати "const" стосовно довідкової кількості (змінної). Поки це було чітко визначено, це було б добре.

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

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


@BenVoigt yep ... замість того, щоб відкидати, це має бути зміненим ... але ви не можете змінити член STL на змінний у похідному класі.
Ерік Аронесті
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.