Чому ми копіюємо тоді переміщення?


98

Десь я побачив код, у якому хтось вирішив скопіювати об’єкт і згодом перемістити його до члена даних класу. Це залишило мене в замішанні в тому, що я думав, що вся справа в тому, щоб уникнути копіювання. Ось приклад:

struct S
{
    S(std::string str) : data(std::move(str))
    {}
};

Ось мої запитання:

  • Чому ми не беремо рецензії на рецензію str?
  • Не буде копія дорогою, особливо, якщо щось подібне std::string?
  • Що було б причиною того, щоб автор вирішив зробити копію, а потім рухатися?
  • Коли я повинен це зробити сам?

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



Відповіді:


97

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

На відміну від C ++ 03, у C ++ 11 часто ідіоматично приймати параметри за значенням з причин, які я збираюся пояснити нижче. Також дивіться це запитання і відповіді на StackOverflow для більш загального набору вказівок щодо прийому параметрів.

Чому ми не беремо рецензії на рецензію str?

Тому що це унеможливить передачу значень, таких як:

std::string s = "Hello";
S obj(s); // s is an lvalue, this won't compile!

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

Не буде копія дорогою, особливо, якщо щось подібне std::string?

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

Отже, підводячи підсумок, два ходи за rvalues, одна копія та одна хода для lvalues.

Що було б причиною того, щоб автор вирішив зробити копію, а потім рухатися?

Перш за все, як я вже згадував вище, перша не завжди є копією; і на це сказано, відповідь така: " Тому що це ефективно (переміщення std::stringпредметів дешеве) і просте ".

За припущенням, що ходи дешеві (ігнорування SSO тут), їх можна практично не враховувати, враховуючи загальну ефективність цієї конструкції. Якщо ми це зробимо, у нас є одна копія для lvalues ​​(як у нас, якби ми прийняли посилання на lvalue const) і жодних копій для rvalues ​​(тоді як у нас все одно буде копія, якби ми прийняли посилання на lvalue const).

Це означає, що взяття за значенням настільки ж добре, як прийняття посилання на значення, constколи вводяться значення, і краще, коли надаються rvalues.

PS: Щоб надати певний контекст, я вважаю, що це питання Q&A, на яке посилається ОП.


2
Варто згадати, що це шаблон C ++ 11, який замінює const T&передачу аргументів: у гіршому випадку (lvalue) це те саме, але у випадку тимчасового вам потрібно лише перемістити тимчасовий. Безпрограшний.
Сем

3
@ user2030677: Ви не можете обійти цю копію, якщо ви не зберігаєте посилання.
Бенджамін Ліндлі

5
@ user2030677: Кого хвилює, наскільки копія дорожча, скільки вам потрібна (а ви це робите, якщо хочете зберегти копію у свого dataчлена)? У вас буде копія, навіть якщо ви посилаєтесь на поважне посилання наconst
Енді Проул

3
@BenjaminLindley: В якості попереднього запису я писав: " За припущенням, що пересування дешеві, їх можна практично не враховувати, розглядаючи загальну ефективність цієї конструкції ". Так, так, не було б накладних витрат, але це слід вважати незначним, якщо немає доказів, що це справжнє занепокоєння, яке виправдовує зміну простого дизайну на щось більш ефективне.
Енді Проул

1
@ user2030677: Але це зовсім інший приклад. У прикладі запитання ви завжди зберігаєте копію data!
Енді Проул

51

Щоб зрозуміти, чому це хороший зразок, ми повинні вивчити альтернативи як в C ++ 03, так і в C ++ 11.

У нас є метод C ++ 03 для взяття std::string const&:

struct S
{
  std::string data; 
  S(std::string const& str) : data(str)
  {}
};

у цьому випадку завжди буде виконана одна копія. Якщо ви побудуєте з необробленого рядка C, то std::stringбуде побудовано, а потім скопійовано знову: два виділення.

Існує метод C ++ 03 взяти посилання на a std::string, а потім поміняти його на локальний std::string:

struct S
{
  std::string data; 
  S(std::string& str)
  {
    std::swap(data, str);
  }
};

це версія C ++ 03 "семантики переміщення", і swapїї часто можна оптимізувати, щоб зробити це дуже дешево (на зразок а move). Це також слід аналізувати в контексті:

S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal

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

struct S
{
  std::string data; 
  S(std::string&& str): data(std::move(str))
  {}
};

Використання:

S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal

Далі ми можемо виконати повну версію C ++ 11, яка підтримує як копію, так і move:

struct S
{
  std::string data; 
  S(std::string const& str) : data(str) {} // lvalue const, copy
  S(std::string && str) : data(std::move(str)) {} // rvalue, move
};

Потім ми можемо вивчити, як це використовується:

S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data

std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data

std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data

Цілком зрозуміло, що ця методика перевантаження 2 принаймні настільки ж ефективна, якщо не більше, ніж два вищевказані стилі C ++ 03. Я буду називати цю 2-перевантажувальну версію "найоптимальнішою" версією.

Тепер ми розглянемо версію взяття за копією:

struct S2 {
  std::string data;
  S2( std::string arg ):data(std::move(x)) {}
};

у кожному з цих сценаріїв:

S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data

std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data

std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data

Якщо ви порівнюєте цю сторону з "найоптимальнішою" версією, ми робимо рівно одну додаткову move! Не раз робимо зайве copy.

Тож якщо припустити, що moveце дешево, ця версія отримує у нас майже таку ж продуктивність, що і найоптимальніша версія, але в 2 рази менше коду.

І якщо ви приймаєте скажімо від 2 до 10 аргументів, зменшення коду є експоненціальним - в 2 рази менше з 1 аргументом, 4x з 2, 8x з 3, 16x з 4, 1024x з 10 аргументами.

Тепер ми можемо обійти це за допомогою ідеального переадресації та SFINAE, що дозволяє вам написати єдиний конструктор або шаблон функції, який бере 10 аргументів, чи SFINAE гарантує, що аргументи мають відповідні типи, а потім переміщує або копіює їх у місцева держава за потребою. Хоча це запобігає збільшенню проблеми розміру програми в тисячу разів, все ще може бути ціла купа функцій, згенерованих із цього шаблону. (Екземпляри функцій шаблона генерують функції)

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

За кілька moveсекунд ми отримуємо коротший код і майже однакову продуктивність, і часто простіший для розуміння код.

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

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


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

6
Оскільки я це писав, на мене вказала ще одна перевага: часто конструктори копій можуть кидати, а конструктори переміщення - часто noexcept. Беручи за допомогою копії даних, ви можете зробити свою функцію noexcept, і будь-яка побудова копії, викликана можливим перекиданням (наприклад, з пам'яті), відбувається поза вашим викликом функції.
Якк - Адам Невраумон

Для чого потрібна версія "lvalue non-const, copy" у техніці 3 перевантаження? Чи не "lvalue const, copy" також не справляється із випадком non const?
Бруно Мартінес

@BrunoMartinez у нас немає!
Якк - Адам Невраумон

13

Це, мабуть, навмисне і схоже на ідіому копіювання та заміни . В основному, оскільки рядок копіюється перед конструктором, сам конструктор є винятком безпечним, оскільки він заміняє (переміщує) тимчасову рядок рядка.


+1 для паралельного копіювання та заміни. Дійсно, вона має багато подібності.
сіам

11

Ви не хочете повторюватися, написавши конструктор для переміщення та один для копії:

S(std::string&& str) : data(std::move(str)) {}
S(const std::string& str) : data(str) {}

Це багато кодових кодів, особливо якщо у вас є кілька аргументів. Ваше рішення дозволяє уникнути дублювання вартості непотрібного кроку. (Однак операція переїзду повинна бути досить дешевою.)

Конкуруюча ідіома полягає у використанні ідеального переадресації:

template <typename T>
S(T&& str) : data(std::forward<T>(str)) {}

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

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

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