Переваги pass-by-value і std :: переміщення через пропускну посилання


106

Зараз я вивчаю С ++ і намагаюся уникати шкідливих звичок. З того, що я розумію, кланг-охайний містить багато "найкращих практик", і я намагаюся дотримуватися їх якнайкраще (хоча я не обов'язково розумію, чому вони ще вважаються хорошими), але я не впевнений, чи зрозумійте, що тут рекомендується.

Я використовував цей клас з підручника:

class Creature
{
private:
    std::string m_name;

public:
    Creature(const std::string &name)
            :  m_name{name}
    {
    }
};

Це призводить до пропозиції clang-tidy, що я повинен передавати значення замість посилання та використання std::move. Якщо я це зробити, я отримую пропозицію зробити nameпосилання (для того, щоб воно не було скопійовано кожен раз) та попередження, яке std::moveне матиме жодного ефекту, тому що nameце constтак, я повинен його видалити.

Єдиний спосіб, коли я не отримую попередження, - це constповністю видалити :

Creature(std::string name)
        :  m_name{std::move(name)}
{
}

Що здається логічним, тому що єдиною перевагою constбуло запобігання возитися з початковою рядком (що не відбувається, тому що я передав значення). Але я читав на CPlusPlus.com :

Хоча зауважте, що в стандартній бібліотеці переміщення означає, що переміщений з об'єкта залишається у дійсному, але не визначеному стані. Що означає, що після такої операції значення переміщеного з об'єкта слід лише знищити або призначити нове значення; доступ до нього в іншому випадку дає неозначене значення.

А тепер уявіть цей код:

std::string nameString("Alex");
Creature c(nameString);

Оскільки nameStringпередається за значенням, std::moveбуде недійсним лише nameвсередині конструктора і не торкнеться початкової рядки. Але в чому переваги цього? Здається, що вміст копіюється лише один раз - якщо я передаю посилання під час дзвінка m_name{name}, якщо передаю значення, коли передаю його (а потім він переміщується). Я розумію, що це краще, ніж пройти повз значення, а не використовувати std::move(тому що він копіюється двічі).

Отже, два питання:

  1. Чи правильно я зрозумів, що тут відбувається?
  2. Чи є перевага використання std::moveпереходу через посилання та просто дзвінка m_name{name}?

3
З пропуском посилання Creature c("John");робить додаткову копію
user253751

1
Це посилання може бути корисним для читання, воно охоплює також проходження std::string_viewта SSO.
лубгр

Я виявив clang-tidy, що це чудовий спосіб заручитися непотрібними мікрооптимізаціями за рахунок читабельності. Питання, яке слід задати тут, перш ніж все інше, полягає в тому, скільки разів ми насправді називаємо Creatureконструктор.
cz

Відповіді:


37
  1. Чи правильно я зрозумів, що тут відбувається?

Так.

  1. Чи є перевага використання std::moveпереходу через посилання та просто дзвінка m_name{name}?

Легкий зрозуміти підпис функції без додаткових перевантажень. Підпис відразу показує, що аргумент буде скопійовано - це рятує абонентів від роздумів, чи const std::string&може посилання зберігатися як член даних, можливо, згодом стає звисаючим посиланням. І не потрібно перевантажувати std::string&& nameта const std::string&аргументувати, щоб уникнути зайвих копій при передачі значень функції у функцію. Передача значення

std::string nameString("Alex");
Creature c(nameString);

до функції, яка приймає свій аргумент за значенням, викликає одну копію та побудову одного ходу. Передача rvalue до тієї ж функції

std::string nameString("Alex");
Creature c(std::move(nameString));

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

Але є і недолік: міркування не працює для функцій, які присвоюють аргумент функції іншій змінній (замість ініціалізації):

void setName(std::string name)
{
    m_name = std::move(name);
}

призведе до розміщення ресурсу, на який m_nameпосилається, до його перепризначення. Я рекомендую прочитати Пункт 41 в "Ефективній сучасній C ++", а також це питання .


Це має сенс, тим більше, що це робить декларацію більш інтуїтивно зрозумілою. Я не впевнений, що я повністю розумію частину вашої відповіді (і розумію зв'язану нитку), тому просто для перевірки. Якщо я використовую move, простір буде розміщено. Якщо я не використовую move, він буде розміщений лише у тому випадку, якщо виділений простір занадто малий, щоб утримувати нову рядок, що призводить до підвищення продуктивності. Це правильно?
Blackbot

1
Так, саме так. При призначенні на m_nameвід const std::string&параметра, внутрішня пам'ять повторно використовуються до тих пір , як m_nameнапади. Коли рух, призначаючи m_name, пам'ять повинна бути вивільнена заздалегідь. В іншому випадку неможливо було "вкрасти" ресурси з правого боку завдання.
лубгр

Коли це стає звисаючим орієнтиром? Я думаю, що список ініціалізації використовує глибоку копію.
Лі Тайцзі

104
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • Пройшов іменують зв'язується з name, потім скопійованої в m_name.

  • Минуло RValue зв'язується з name, потім скопійованої в m_name.


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • Передаються іменують буде скопійований в name, потім переміщений в m_name.

  • Успішне складання Rvalue буде переміщений в name, потім переїхав в m_name.


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • Пройшов іменують зв'язується з name, потім скопійованої в m_name.

  • Минуло RValue зв'язується з rname, потім перемістилися в m_name.


Оскільки операції переміщення зазвичай швидші, ніж копії, (1) краще, ніж (0), якщо ви передаєте багато тем. (2) є оптимальним з точки зору копій / рухів, але вимагає повторення коду.

Повторення коду можна уникнути за допомогою ідеального переадресації :

/* (3) */
template <typename T,
          std::enable_if_t<
              std::is_convertible_v<std::remove_cvref_t<T>, std::string>, 
          int> = 0
         >
Creature(T&& name) : m_name{std::forward<T>(name)} { }

Ви, можливо, захочете обмежитися T, щоб обмежити домен типів, з якими цей конструктор може бути інстанційований (як показано вище). C ++ 20 має на меті спростити це за допомогою концепцій .


У C ++ 17 на цінність впливає гарантована елізія копії , яка - при застосуванні - зменшить кількість копій / ходів при передачі аргументів функціям.


Для (1) значення pr-значення та значення xvalue не ідентичні, оскільки c ++ 17 немає?
Олів

1
Зауважте, що вам не потрібно SFINAE, щоб вдосконалити вперед у цьому випадку. Це потрібно лише для роз'єднання. Це правдоподібно корисно для потенційних повідомлень про помилки при передачі поганих аргументів
Caleth

@Oliv Так. xvalues ​​потрібно перемістити, тоді як prvalues ​​можна усунути :)
Rakete1111,

1
Чи можемо ми записати: Creature(const std::string &name) : m_name{std::move(name)} { }у (2) ?
skytree

4
@skytree: ви не можете переміститися з об'єкта const, оскільки переміщення мутує джерело. Це складе, але зробить копію.
Вітторіо Ромео

1

Те, як ви проходите, не є єдиною змінною тут, те , що ви передаєте, має велику різницю між ними.

У C ++, у нас є всі види вартісних категорій , і це «ідіома» існує для випадків , коли ви проходите в RValue (наприклад , як "Alex-string-literal-that-constructs-temporary-std::string"чи std::move(nameString)), що призводить до 0 копій в std::stringробляться (тип , навіть не повинен бути копією конструктивна для аргументів rvalue) і використовує лише std::stringконструктор переміщення.

Дещо пов'язані питання та відповіді .


1

Існує декілька недоліків підходу пропускної вартості та переміщення над посиланням «пройти повз» (rv):

  • це спричиняє породження 3 об’єктів замість 2;
  • передача об'єкта за значенням може призвести до додаткових накладних стеків, оскільки навіть звичайний клас рядків, як правило, принаймні в 3 або 4 рази більший, ніж покажчик;
  • Побудова об’єктів аргументу буде здійснюватися на стороні виклику, викликаючи розрив коду;

Не могли б ви пояснити, чому це призведе до появи нересту 3 об’єкта? З того, що я розумію, я можу просто передати "Пітер" як струну. Це стане породженим, скопійованим, а потім перенесеним, чи не так? І чи не став би стек використовуватись у якийсь момент незалежно? Не в точці виклику конструктора, а в тій m_name{name}частині, де він копіюється?
Blackbot

@Blackbot Я мав на увазі ваш приклад, std::string nameString("Alex"); Creature c(nameString);один об'єкт - це nameStringінший аргумент функції, а третій - поле класу.
користувач7860670

0

У моєму випадку перемикання на передачу за значенням, а потім виконання std: переміщення викликало помилку без нагромадження без використання в Address Sanitizer.

https://travis-ci.org/github/acgetchell/CDT-plusplus/jobs/679520360#L3165

Отже, я це відключив, як і пропозицію в кланг-охайному.

https://github.com/acgetchell/CDT-plusplus/compare/80c96789f0a2...0d78fd63b332

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