Щоб зрозуміти, чому це хороший зразок, ми повинні вивчити альтернативи як в 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
в аргументі, щоб контролювати, де відбувається кидання). Створювати методи нерозвинення часто варто.