Я щойно закінчив слухати інтерв'ю радіо подкастів Software Engineering з Скоттом Майєрсом щодо C ++ 0x . Більшість нових функцій мали для мене сенс, і я зараз захоплююсь C ++ 0x, за винятком однієї. Я все ще не отримую семантики руху ... Що це саме?
Я щойно закінчив слухати інтерв'ю радіо подкастів Software Engineering з Скоттом Майєрсом щодо C ++ 0x . Більшість нових функцій мали для мене сенс, і я зараз захоплююсь C ++ 0x, за винятком однієї. Я все ще не отримую семантики руху ... Що це саме?
Відповіді:
Мені найпростіше зрозуміти переміщення семантики з прикладом коду. Почнемо з дуже простого рядкового класу, який містить лише вказівник на виділений в пам'ять блок пам'яті:
#include <cstring>
#include <algorithm>
class string
{
char* data;
public:
string(const char* p)
{
size_t size = std::strlen(p) + 1;
data = new char[size];
std::memcpy(data, p, size);
}
Оскільки ми вирішили керувати пам’яттю самостійно, нам потрібно дотримуватися правила трьох . Я збираюся відкласти написання оператора призначення і зараз реалізуватиму лише деструктор та конструктор копій:
~string()
{
delete[] data;
}
string(const string& that)
{
size_t size = std::strlen(that.data) + 1;
data = new char[size];
std::memcpy(data, that.data, size);
}
Конструктор копіювання визначає, що означає копіювати рядкові об'єкти. Параметр const string& that
пов'язує всі вирази рядка типу, що дозволяє робити копії в наступних прикладах:
string a(x); // Line 1
string b(x + y); // Line 2
string c(some_function_returning_a_string()); // Line 3
Тепер з'являється ключовий погляд на семантику руху. Зауважте, що лише в першому рядку, де ми копіюємоx
, ця глибока копія дійсно необхідна, тому що ми могли б хотіти перевірити x
пізніше і були б дуже здивовані, якби x
якимось чином змінився. Ви помічали, як я щойно сказав x
тричі (чотири рази, якщо включити це речення) і означав кожен і той самий предмет ? Ми називаємо такі вирази, якx
"lvalues".
Аргументи в рядках 2 і 3 - це не значення, а rvalues, оскільки основні рядкові об'єкти не мають імен, тому клієнт не має можливості перевірити їх ще раз у наступний момент часу. rvalues позначає тимчасові об'єкти, які знищуються в наступній крапці з комою (якщо бути точнішою: наприкінці повного виразу, який лексично містить rvalue). Це важливо, тому що під час ініціалізації b
та c
, ми могли робити все, що завгодно, із вихідним рядком, і клієнт не міг визначити різницю !
C ++ 0x вводить новий механізм під назвою "посилання на значення", який, серед іншого, дозволяє виявити аргументи rvalue через перевантаження функції. Все, що нам потрібно зробити, - це написати конструктор з контрольним параметром rvalue. Всередині цього конструктора ми можемо робити все, що завгодно з джерелом, доки ми залишаємо його в якомусь дійсному стані:
string(string&& that) // string&& is an rvalue reference to a string
{
data = that.data;
that.data = nullptr;
}
Що ми тут зробили? Замість того, щоб глибоко копіювати дані купи, ми просто скопіювали покажчик, а потім встановили початковий вказівник на нуль (щоб запобігти видаленню [] 'з деструктора вихідного об’єкта не випустити наші «щойно вкрадені дані»). Насправді ми "вкрали" дані, які спочатку належали до вихідного рядка. Знову ж таки, ключовим розумінням є те, що ні за яких обставин клієнт не міг виявити, що джерело було змінено. Оскільки ми насправді не робимо копію тут, ми називаємо цей конструктор "конструктором переміщення". Його завдання полягає в переміщенні ресурсів від одного об’єкта до іншого, а не копіювання їх.
Вітаємо, тепер ви розумієте основи семантики ходу! Давайте продовжимо, реалізуючи оператор призначення. Якщо ви не знайомі з ідіомою копіювання та заміни , вивчіть її та поверніться, тому що це дивовижна ідіома C ++, пов’язана із безпекою виключень.
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
};
Ага, це все? "Де посилання на рецензію?" ви можете запитати. "Нам тут це не потрібно!" моя відповідь :)
Зауважте, що ми передаємо параметр that
за значенням , тому that
його потрібно ініціалізувати, як і будь-який інший рядковий об'єкт. Як саме that
буде ініціалізовано? За старих часів С ++ 98 відповідь була б "конструктором копій". У C ++ 0x компілятор вибирає між конструктором копіювання та конструктором переміщення, виходячи з того, чи є аргументом оператору присвоєння значення lvalue або rvalue.
Отже, якщо ви скажете a = b
, конструктор копій ініціалізується that
(тому що виразb
є значенням), і оператор присвоєння змістить вміст на щойно створену, глибоку копію. Це саме визначення ідіоми копії та підміни - зробити копію, поміняти вміст копією, а потім позбутися копії, залишивши область застосування. Тут нічого нового.
Але якщо ви скажете a = x + y
, конструктор переміщення буде ініціалізуватися that
(тому що вираз x + y
є оцінкою), тому немає жодної глибокої копії, а лише ефективний хід.
that
все ще є незалежним об'єктом від аргументу, але його побудова була тривіальною, оскільки дані купи не потрібно було копіювати, а просто переміщувати. Копіювати його не потрібно було, тому що x + y
це ревальвація, і знову ж таки, добре переходити від рядкових об'єктів, позначених rvalues.
Підводячи підсумок, конструктор копій робить глибоку копію, оскільки джерело має залишатися недоторканим. Конструктор переміщення, з іншого боку, може просто скопіювати покажчик, а потім встановити вказівник у джерелі на нуль. Добре «звести нанівець» вихідний об’єкт таким чином, оскільки у клієнта немає можливості знову перевірити об’єкт.
Я сподіваюся, що цей приклад отримав основну точку в цілому. Існує набагато більше для того, щоб оцінити посилання та перенести семантику, яку я навмисно лишив, щоб зробити це просто. Якщо ви хочете отримати більше деталей, будь ласка, дивіться мою додаткову відповідь .
that.data = 0
цього персонажів було б знищено занадто рано (коли тимчасові помирають), а також двічі. Ви хочете вкрасти дані, а не ділитися ними!
delete[]
на nullptr визначається стандартом C ++, щоб бути неоперативним.
Першою моєю відповіддю було надзвичайно спрощене введення для переміщення семантики, і багато деталей було замислено спеціально, щоб зробити це просто. Однак для семантики зрушити ще багато, і я подумав, що настав час другої відповіді, щоб заповнити прогалини. Перша відповідь вже досить стара, і не здавалося правильним просто замінити її зовсім іншим текстом. Я думаю, що це все ще добре служить першим вступом. Але якщо ви хочете копати глибше, читайте далі :)
Стефан Т. Лававей знайшов час, щоб надати цінні відгуки. Дуже дякую, Стефане!
Семантика переміщення дозволяє об’єкту за певних умов брати право власності на зовнішні ресурси якогось іншого об'єкта. Це важливо двома способами:
Перетворення дорогих копій в дешеві рухи. Дивіться для прикладу мою першу відповідь. Зауважте, що якщо об’єкт не керує хоча б одним зовнішнім ресурсом (безпосередньо чи опосередковано через об'єкти його учасника), семантика переміщення не надасть жодних переваг перед семантикою копіювання. У такому випадку копіювання об'єкта та переміщення об'єкта означає саме те саме:
class cannot_benefit_from_move_semantics
{
int a; // moving an int means copying an int
float b; // moving a float means copying a float
double c; // moving a double means copying a double
char d[64]; // moving a char array means copying a char array
// ...
};
Впровадження безпечних типів "лише для переміщення"; тобто типи, для яких копіювання не має сенсу, але переміщення робить. Приклади включають блокування, ручки файлів та розумні покажчики з унікальною семантикою власності. Примітка. У цій відповіді обговорюється std::auto_ptr
застарілий стандартний шаблон бібліотеки C ++ 98, який був замінений на std::unique_ptr
C ++ 11. Проміжні програмісти C ++, мабуть, принаймні дещо знайомі std::auto_ptr
, і через відображену "семантику переміщення" це здається гарною відправною точкою для обговорення семантики переміщення в C ++ 11. YMMV.
Стандартна бібліотека C ++ 98 пропонує розумний покажчик з унікальною семантикою власності std::auto_ptr<T>
. У випадку, якщо ви не знайомі auto_ptr
, його мета - гарантувати, що динамічно виділений об'єкт буде завжди випущений, навіть за умови винятків:
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
Незвичайне в тому auto_ptr
, що вона "копіює" поведінку:
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
Зверніть увагу , як ініціалізація b
з a
зовсім НЕ копіювати трикутник, але замість цього передає право власності на трикутнику від a
до b
. Ми також говорять , що « a
буде переміщений в b
" або "трикутник переміщається з a
до b
». Це може здатися заплутаним, оскільки сам трикутник завжди залишається на тому самому місці в пам'яті.
Перемістити об’єкт означає передати право власності на якийсь ресурс, яким він управляє, на інший об’єкт.
Конструктор копій, auto_ptr
мабуть, виглядає приблизно так (дещо спрощено):
auto_ptr(auto_ptr& source) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
Небезпечне в тому auto_ptr
, що те, що синтаксично виглядає як копія, насправді є ходом. Спроба викликати функцію члена при переміщенні auto_ptr
буде викликати невизначене поведінку, тож вам слід бути дуже обережним, щоб не використовувати посилання auto_ptr
після її переміщення з:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
Але auto_ptr
це не завжди небезпечно. Заводські функції - прекрасний випадок використання для auto_ptr
:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
Зверніть увагу, як обидва приклади дотримуються однакової синтаксичної схеми:
auto_ptr<Shape> variable(expression);
double area = expression->area();
І все ж, одна з них посилається на невизначену поведінку, тоді як інша - ні. То яка різниця між виразами a
та make_triangle()
? Чи не вони обидва одного типу? Дійсно вони є, але вони мають різні ціннісні категорії .
Очевидно, що між виразом, a
який позначає auto_ptr
змінну, і виразом, make_triangle()
який позначає виклик функції, яка повертає auto_ptr
значення, має бути деяка глибока різниця , що створює свіжий тимчасовий auto_ptr
об'єкт кожного разу, коли він викликається. a
є прикладом значення , тоді як make_triangle()
є прикладом ревальва .
Перехід від таких значень, як a
небезпечний, тому що ми могли б спробувати викликати функцію члена через a
, посилаючись на невизначену поведінку. З іншого боку, перехід від таких значень, як make_triangle()
цілком безпечний, оскільки після того, як конструктор копій зробив свою роботу, ми не можемо використовувати тимчасові знову. Не існує вираження, який би позначав сказане тимчасове; якщо ми просто make_triangle()
знову напишемо , отримаємо інший тимчасовий. Насправді переміщений з тимчасового вже перейшов у наступний рядок:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Зверніть увагу , що букви l
і r
мають історичне походження в стороні лівої руки і правої частини присвоювання. Це більше не відповідає дійсності для C ++, оскільки є значення, які не можуть з’являтися з лівої сторони завдання (наприклад, масиви або визначені користувачем типи без оператора присвоєння), і є rvalues, які можуть (усі rvalues типів класів з оператором призначення).
Оцінка типу класу - це вираз, оцінка якого створює тимчасовий об'єкт. За звичайних обставин жодне інше вираження всередині того самого діапазону не позначає той же тимчасовий об'єкт.
Тепер ми розуміємо, що перехід від значень значення потенційно небезпечний, але переміщення з числа оцінок нешкідливий. Якщо C ++ мала мовну підтримку для відрізнення аргументів lvalue від аргументів rvalue, ми могли б або повністю заборонити перехід від lvalues, або принаймні зробити перехід від lvalues явним на сайті виклику, щоб ми більше не рухалися випадково.
Відповідь C ++ 11 на цю проблему - це релевантні посилання . Посилання на rvalue - це новий вид посилань, який пов'язується лише з rvalues, а синтаксис є X&&
. Стара хороша довідка X&
тепер відома як посилання на значення . (Зверніть увагу, що X&&
це не посилання на посилання; у C ++ такого немає.)
Якщо ми кинемо const
в суміш, у нас вже є чотири різних посилання. До яких типів виразів типу X
вони можуть зв’язатись?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
На практиці можна забути const X&&
. Бути обмеженим на читання з rvalues не дуже корисно.
Посилання на оцінку
X&&
- це новий вид посилань, який пов'язується лише з rvalues.
Довідники Rvalue проходили через кілька версій. Починаючи з версії 2.1, посилання rvalue X&&
також пов'язує всі категорії значень іншого типу за Y
умови неявного перетворення з Y
у X
. У цьому випадку створюється тимчасовий тип X
, і посилання rvalue пов'язане з тимчасовим:
void some_function(std::string&& r);
some_function("hello world");
У наведеному вище прикладі "hello world"
є значення типу const char[12]
. Оскільки відбувається неявна конверсія від const char[12]
наскрізного const char*
до std::string
, створюється тимчасовий тип std::string
, r
який пов'язаний з тимчасовим. Це один із випадків, коли відмінність між rvalues (виразами) та temporaries (об'єктами) трохи розмито.
Корисним прикладом функції з X&&
параметром є конструктор переміщення X::X(X&& source)
. Його мета - передати право власності на керований ресурс від джерела до поточного об'єкта.
У C ++ 11 std::auto_ptr<T>
було замінено, std::unique_ptr<T>
що використовує переваги посилання rvalue. Я буду розробляти і обговорювати спрощену версію unique_ptr
. По-перше, ми інкапсулюємо необроблений покажчик і перевантажимо операторів, ->
і *
тому наш клас відчуває себе вказівником:
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
Конструктор бере право власності на об'єкт, а деструктор видаляє його:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
Тепер приходить цікава частина конструктора ходу:
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
Цей конструктор переміщення робить саме те, що auto_ptr
зробив конструктор копіювання, але він може постачатися лише з rvalues:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
Другий рядок не вдається зібрати, тому що a
це lvalue, але параметр unique_ptr&& source
може бути прив'язаний лише до rvalues. Це саме те, що ми хотіли; небезпечні рухи ніколи не повинні бути неявними. Третій рядок компілюється просто чудово, тому що make_triangle()
це рецензія. Конструктор переміщення перенесе право власності від тимчасового до c
. Знову ж таки, саме цього ми хотіли.
Конструктор переміщення передає право власності на керований ресурс поточному об'єкту.
Останній відсутній фрагмент - оператор присвоєння переміщення. Його завдання полягає у звільненні старого ресурсу та придбанні нового ресурсу зі свого аргументу:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
Зверніть увагу, як ця реалізація оператора призначення переміщення дублює логіку як деструктора, так і конструктора переміщення. Чи знайомі ви з ідіомою копіювання та заміни? Його також можна застосувати для переміщення семантики як ідіоми переміщення та заміни:
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
Тепер, що source
є змінною типу unique_ptr
, вона буде ініціалізована конструктором переміщення; тобто аргумент буде переміщено в параметр. Аргумент як і раніше повинен бути rvalue, оскільки сам конструктор переміщення має опорний параметр rvalue. Коли потік управління досягає закриває фігурну дужку operator=
, source
виходить з області видимості, автоматично звільняючи старий ресурс.
Оператор призначення переміщення передає право власності на керований ресурс на поточний об'єкт, звільняючи старий ресурс. Ідіома переміщення та заміни спрощує реалізацію.
Іноді ми хочемо перейти від значень. Тобто, іноді ми хочемо, щоб компілятор ставився до lvalue так, як якщо б це було rvalue, тому він може викликати конструктор переміщення, хоча це може бути небезпечно. З цією метою C ++ 11 пропонує стандартний шаблон функції бібліотеки, який називається std::move
всередині заголовка <utility>
. Це ім'я трохи прикро, тому що std::move
просто кидає value на rvalue; вона нічого не рухає сама по собі. Це просто дозволяє рухатись. Можливо, воно повинно було бути названим std::cast_to_rvalue
або std::enable_move
, але ми до цього часу назріли.
Ось як явно ви переходите від значення lvalue:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
Зауважте, що після третього рядка a
більше немає власного трикутника. Це нормально, тому що явно писати std::move(a)
, ми зробили наші наміри ясно: «Дорогий конструктора, робити все , що ви хочете з a
для ініціалізації c
, я не дбаю про a
більше Чи не соромтеся , щоб мати свій шлях з. a
.»
std::move(some_lvalue)
кидає lvalue на rvalue, таким чином, даючи можливість наступного переміщення.
Зауважте, що хоч std::move(a)
це і є ревальвація, його оцінка не створює тимчасового об'єкта. Ця загадка змусила комітет запровадити третю ціннісну категорію. Щось, що може бути пов'язане з посиланням на rvalue, навіть якщо воно не є rvalue у традиційному розумінні, називається xvalue (значення eXpiring). Традиційні rvalues були перейменовані на prvalues (Чисті rvalues).
І prvalues, і xvalues є rvalues. Xvalues і lvalues - це обидва glvalues (Узагальнені lvalues). Стосунки легше зрозуміти за допомогою діаграми:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Зауважте, що лише xvalues дійсно нові; решта саме завдяки перейменуванню та групуванню.
C ++ 98 rvalues відомі як prvalues у C ++ 11. Подумки замініть всі випадки "rvalue" у попередніх пунктах на "prvalue".
Поки ми спостерігали рух як в локальні змінні, так і в параметри функцій. Але рух можливий і в зворотному напрямку. Якщо функція повертається за значенням, деякий об'єкт на сайті виклику (можливо, локальна змінна або тимчасовий, але може бути будь-який тип об'єкта) ініціалізується з виразом після return
оператора як аргумент конструктору переміщення:
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
Можливо, дивно, що автоматичні об'єкти (локальні змінні, які не оголошені як static
), також можуть бути неявно виведені з функцій:
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
Чому конструктор ходу приймає lvalue result
як аргумент? Область дії result
вже закінчиться, і вона буде знищена під час розмотування стека. Після цього ніхто не міг поскаржитися, що result
якось змінилося; коли потік управління повертається у абонента, result
вже не існує! З цієї причини C ++ 11 має спеціальне правило, яке дозволяє повертати автоматичні об'єкти з функцій без необхідності запису std::move
. Насправді, ви ніколи не повинні використовувати std::move
автоматичні об'єкти для виведення з функцій, оскільки це гальмує "названу оптимізацію зворотного значення" (NRVO).
Ніколи не використовуйте
std::move
для переміщення автоматичних об'єктів з функцій.
Зауважте, що в обох заводських функціях тип повернення - це значення, а не посилання на значення. Посилання Rvalue все ще є посиланнями, і як завжди, ви ніколи не повинні повертати посилання на автоматичний об'єкт; абонент отримав би звичку, якщо ви обдурили компілятор у прийнятті вашого коду, наприклад:
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
Ніколи не повертайте автоматичні об'єкти за посиланням на rvalue. Переміщення виконується виключно конструктором переміщення, а не шляхом
std::move
, а не просто прив'язкою rvalue до посилання rvalue.
Рано чи пізно ви збираєтеся написати такий код:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
В основному, компілятор буде скаржитися, що parameter
це значення. Якщо ви подивитесь на його тип, ви побачите посилання на оцінку rvalue, але посилання на rvalue просто означає "посилання, яке пов'язане з rvalue"; це не означає, що саме посилання є релевантним! Дійсно, parameter
це просто звичайна змінна з назвою. Ви можете користуватися parameter
якомога частіше всередині корпусу конструктора, і він завжди позначає один і той же об'єкт. Швидке переміщення від неї було б небезпечно, тому мова забороняє це.
Іменоване посилання rvalue - це значення, як і будь-яка інша змінна.
Рішення полягає в ручному включенні переміщення:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // note the std::move
{}
};
Ви можете стверджувати, що parameter
більше не використовується після ініціалізації member
. Чому немає спеціального правила, щоб мовчки вставлятиstd::move
само, як і значення повернення? Можливо, тому, що це буде занадто великим навантаженням на виконавці компілятора. Наприклад, що робити, якщо корпус конструктора знаходився в іншому блоці перекладу? На противагу цьому, правило повернення значення має просто перевірити таблиці символів, щоб визначити, чи відповідає ідентифікатор після того, як return
ключове слово позначає автоматичний об'єкт.
Ви також можете передавати parameter
значення за значенням. Для таких типів, як лише для переміщенняunique_ptr
, схоже, що ще немає встановленої ідіоми. Особисто я віддаю перевагу передачі за значенням, оскільки це викликає менше захаращення в інтерфейсі.
C ++ 98 неявно оголошує три функції спеціальних членів на вимогу, тобто коли вони потрібні десь: конструктор копій, оператор присвоєння копії та деструктор.
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
Довідники Rvalue проходили через кілька версій. Починаючи з версії 3.0, C ++ 11 оголошує дві додаткові функції спеціальних членів на вимогу: конструктор переміщення та оператор присвоєння переміщення. Зауважте, що ні VC10, ні VC11 не відповідають версії 3.0, тому вам доведеться їх реалізовувати самостійно.
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
Ці дві нові функції спеціальних членів декларуються лише неявно, якщо жодна з функцій спеціального члена не оголошується вручну. Крім того, якщо ви оголосите власного конструктора переміщення або оператора присвоєння переміщення, ні конструктор копій, ні оператор призначення копії не будуть оголошені неявно.
Що ці правила означають на практиці?
Якщо ви пишете клас без некерованих ресурсів, немає необхідності самостійно оголошувати будь-яку з п'яти спеціальних функцій учасників, і ви отримаєте правильну семантику копіювання та переміщення семантики безкоштовно. В іншому випадку вам доведеться самостійно реалізовувати функції спеціального члена. Звичайно, якщо ваш клас не має переваги в семантиці переміщення, немає необхідності в реалізації спеціальних операцій переміщення.
Зауважте, що оператор призначення копії та оператор присвоєння переміщення можуть бути об'єднані в єдиний об'єднаний оператор присвоєння, приймаючи його аргумент за значенням:
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
Таким чином, кількість спеціальних функцій членів для реалізації крапель з п'яти до чотирьох. Тут є компроміс між безпекою та винятком винятків, але я не є експертом у цьому питанні.
Розглянемо наступний шаблон функції:
template<typename T>
void foo(T&&);
Ви можете розраховувати T&&
на прив’язку лише до значень, оскільки на перший погляд це виглядає як посилання на оцінку. Як виявляється, хоча, T&&
також прив'язується до значень:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
Якщо аргумент є ревальваційним типом X
, T
виводиться, щоб бути X
, значить, T&&
означає X&&
. Це те, що хтось очікував. Але якщо аргумент є типовим значенням X
, обумовленим спеціальним правилом, T
випливає X&
, T&&
що означає щось подібне X& &&
. Але так як C ++ до сих пір поняття не має посилань на посилання, тип X& &&
буде зруйнувалася в X&
. Спочатку це може здатися заплутаним і марним, але згортання посилань має важливе значення для ідеального пересилання (про що тут мова не піде).
T&& - це не релевантна посилання, а посилання на переадресацію. Він також зв'язується з lvalues, в якому випадку
T
іT&&
обидва Lvalue посилання.
Якщо ви хочете обмежити шаблон функції до значень, ви можете комбінувати SFINAE з ознаками типу:
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Тепер, коли ви розумієте згортання посилань, ось як std::move
реалізовано:
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Як бачите, move
приймає будь-який параметр завдяки посиланню на переадресацію T&&
, і він повертає посилання rvalue. std::remove_reference<T>::type
Виклик мета-функція необхідна , так як в противному випадку, для lvalues типу X
, тип повертається б X& &&
, що б впасти в X&
. Оскільки t
завжди є значення lvalue (пам’ятайте, що названа посилання rvalue є рівнем lvalue), але ми хочемо прив’язати t
до посилання rvalue, ми повинні явно t
передати правильний тип повернення. Виклик функції, що повертає посилання rvalue, сам по собі є xvalue. Тепер ви знаєте, звідки беруться xvalues;)
Виклик функції, яка повертає посилання rvalue, наприклад
std::move
, xvalue.
Зауважте, що повернення за допомогою посилання rvalue добре в цьому прикладі, оскільки t
не позначає автоматичний об'єкт, а замість цього об'єкт, переданий абонентом.
Семантика переміщення заснована на посиланнях на оцінку .
Rvalue - це тимчасовий об'єкт, який буде знищений в кінці виразу. У поточному C ++ rvalues прив'язує лише до const
посилань. C ++ 1x дозволить не- const
оцінювати посилання, написані T&&
як посилання на об'єкти, що оцінюються.
Оскільки rvalue загине в кінці виразу, ви можете вкрасти його дані . Замість того, щоб копіювати його в інший об’єкт, ви переміщуєте його дані в нього.
class X {
public:
X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
: data_()
{
// since 'x' is an rvalue object, we can steal its data
this->swap(std::move(rhs));
// this will leave rhs with the empty data
}
void swap(X&& rhs);
// ...
};
// ...
X f();
X x = f(); // f() returns result as rvalue, so this calls move-ctor
У наведеній вище коді, зі старими компіляторами результат f()
буде скопійований в x
використанні X
«s конструктора копіювання. Якщо ваш компілятор підтримує семантику переміщення і X
має конструктор move, тоді це називається замість цього. Оскільки його rhs
аргумент є значущим , ми знаємо, що він більше не потрібен, і ми можемо вкрасти його значення.
Таким чином, значення переміщується з неназваного тимчасового, поверненого з f()
до x
(в той час як дані x
, ініціалізовані в порожнє X
, переміщуються в тимчасові, які після присвоєння будуть знищені).
this->swap(std::move(rhs));
тому, що названі посилання rvalue
rhs
це значення в контексті X::X(X&& rhs)
. Вам потрібно зателефонувати, std::move(rhs)
щоб отримати оцінку, але ця ситуація робить відповідь суперечкою.
Припустимо, у вас є функція, яка повертає істотний об'єкт:
Matrix multiply(const Matrix &a, const Matrix &b);
Коли ви пишете такий код:
Matrix r = multiply(a, b);
тоді звичайний компілятор C ++ створить тимчасовий об’єкт для результату multiply()
, викличе конструктор копій для ініціалізації r
та знищить тимчасове повернене значення. Семантика переміщення в C ++ 0x дозволяє викликати "конструктор переміщення" для ініціалізаціїr
, копіюючи його вміст, а потім відкидає тимчасове значення, не руйнуючи його.
Це особливо важливо, якщо (як, можливо, Matrix
приклад вище) об'єкт, який копіюється, виділяє додаткову пам’ять на купі для зберігання його внутрішнього подання. Конструктору копій доведеться або зробити повну копію внутрішнього представлення, або використовувати семантику підрахунку посилань та семантику копіювання на запис цілком. Конструктор переміщення залишив би пам'ять купи в спокої і просто скопіював покажчик всередині Matrix
об'єкта.
Якщо ви дійсно зацікавлені в хорошому, глибокому поясненні семантики переміщення, я настійно рекомендую прочитати на них оригінальний документ "Пропозиція додати підтримку Move Semantics до мови C ++".
Це дуже доступно і легко для читання, і це прекрасний варіант для переваг, які вони пропонують. На веб-сайті WG21 доступні й інші новітні та актуальні статті про семантику переміщень , але ця, мабуть, найпростіша, оскільки вона наближається до речей з точки зору верхнього рівня та не надто детально вкладається в стислі мови.
Семантика руху - це передача ресурсів, а не їх копіювання, коли вже нікому не потрібне значення джерела.
У C ++ 03 об'єкти часто копіюються, лише їх знищують або передають перед тим, як будь-який код знову використовує це значення. Наприклад, коли ви повертаєтесь за значенням з функції - якщо RVO не запускається - значення, яке ви повертаєте, копіюється в кадр стека абонента, а потім воно виходить за межі і знищується. Це лише один із багатьох прикладів: див. Прохідне значення, коли вихідний об'єкт є тимчасовим, такі алгоритми sort
просто переставляють елементи, перерозподіляють, vector
коли його capacity()
перевищують тощо.
Коли такі копіювати / знищувати пари дорого, це зазвичай тому, що об'єкт володіє деяким важким ресурсом. Наприклад, vector<string>
може бути власник динамічно виділеного блоку пам'яті, що містить масив string
об'єктів, кожен з яких має власну динамічну пам'ять. Копіювання такого об’єкта коштує дорого: ви повинні виділити нову пам'ять для кожного динамічно виділених блоків у джерелі та скопіювати всі значення впоперек. Тоді вам потрібно розібрати всю пам’ять, яку ви щойно скопіювали. Однак переміщення великого vector<string>
означає просто скопіювати кілька покажчиків (які відносяться до блоку динамічної пам'яті) до місця призначення та нулювати їх у джерело.
Простим (практичним) терміном:
Копіювання об'єкта означає копіювання його "статичних" членів та виклик new
оператора для його динамічних об'єктів. Правильно?
class A
{
int i, *p;
public:
A(const A& a) : i(a.i), p(new int(*a.p)) {}
~A() { delete p; }
};
Однак переміщення об’єкта (повторююсь, з практичної точки зору) означає лише скопіювати покажчики динамічних об’єктів, а не створювати нові.
Але хіба це не небезпечно? Звичайно, ви можете знищити динамічний об’єкт двічі (помилка сегментації). Отже, щоб уникнути цього, слід "визнати недійсними" вихідні покажчики, щоб уникнути їх знищення двічі:
class A
{
int i, *p;
public:
// Movement of an object inside a copy constructor.
A(const A& a) : i(a.i), p(a.p)
{
a.p = nullptr; // pointer invalidated.
}
~A() { delete p; }
// Deleting NULL, 0 or nullptr (address 0x0) is safe.
};
Гаразд, але якщо я переміщу об’єкт, вихідний об'єкт стає марним, ні? Звичайно, але в певних ситуаціях це дуже корисно. Найбільш очевидним є те, коли я викликаю функцію з анонімним об'єктом (тимчасовий, об'єкт rvalue, ..., ви можете називати його різними іменами):
void heavyFunction(HeavyType());
У цій ситуації створюється анонімний об'єкт, який далі копіюється в параметр функції, а потім видаляється. Отже, тут краще перемістити об’єкт, оскільки анонімний об’єкт вам не потрібен, і ви можете заощадити час і пам'ять.
Це призводить до поняття "рейтингової" посилання. Вони існують у C ++ 11 лише для виявлення, чи отриманий об’єкт анонімний чи ні. Я думаю, ви вже знаєте, що "lvalue" - це об'єкт, що присвоюється (ліва частина =
оператора), тому вам потрібна посилання на об'єкт, щоб він міг діяти як значення. Оцінка - це навпаки, об'єкт без названих посилань. Через це анонімний об'єкт і rvalue є синонімами. Тому:
class A
{
int i, *p;
public:
// Copy
A(const A& a) : i(a.i), p(new int(*a.p)) {}
// Movement (&& means "rvalue reference to")
A(A&& a) : i(a.i), p(a.p)
{
a.p = nullptr;
}
~A() { delete p; }
};
У цьому випадку, коли об’єкт типу A
повинен бути "скопійований", компілятор створює посилання lvalue або посилання rvalue відповідно до того, чи переданий об'єкт названий чи ні. Якщо ні, то ваш конструктор переміщення викликається, і ви знаєте, що об'єкт є тимчасовим, і ви можете переміщати його динамічні об'єкти, а не копіювати їх, економлячи простір та пам'ять.
Важливо пам’ятати, що «статичні» об’єкти завжди копіюються. Немає способів "перемістити" статичний об'єкт (об'єкт у стеці, а не в купі). Отже, відмінність "переміщення" / "копія", коли об'єкт не має динамічних членів (прямо чи опосередковано), не має значення.
Якщо ваш об’єкт складний, а деструктор має інші вторинні ефекти, наприклад, виклик функції бібліотеки, виклик до інших глобальних функцій або що б там не було, можливо, краще подати сигнал про рух прапором:
class Heavy
{
bool b_moved;
// staff
public:
A(const A& a) { /* definition */ }
A(A&& a) : // initialization list
{
a.b_moved = true;
}
~A() { if (!b_moved) /* destruct object */ }
};
Отже, ваш код коротший (вам не потрібно виконувати nullptr
завдання для кожного динамічного члена) та більш загальний.
Інше типове питання: в чому різниця між A&&
і const A&&
? Звичайно, у першому випадку ви можете змінити об’єкт, а в другому - не, але, практичне значення? У другому випадку ви не можете його змінити, тож у вас немає способів визнати об'єктом недійсним (за винятком змінного прапора або чогось подібного), і практичної різниці у конструкторі копій немає.
І що ідеальне переадресація ? Важливо знати, що "посилання на оцінку" - це посилання на іменований об'єкт у "області виклику". Але в реальному масштабі посилання rvalue - це ім'я до об'єкта, тому він виступає як названий об'єкт. Якщо ви передаєте посилання rvalue на іншу функцію, ви передаєте іменований об'єкт, тому об'єкт не отримується як часовий об'єкт.
void some_function(A&& a)
{
other_function(a);
}
Об'єкт a
буде скопійовано у фактичний параметр other_function
. Якщо ви хочете, щоб об’єкт a
продовжував розглядатись як тимчасовий об'єкт, вам слід скористатися std::move
функцією:
other_function(std::move(a));
За допомогою цього рядка std::move
буде a
присвоєно значення і other_function
отримає об'єкт як неназваний об'єкт. Звичайно, якщо other_function
немає специфічної перевантаження для роботи з неназваними об'єктами, ця відмінність не важлива.
Це ідеальне переадресація? Ні, але ми дуже близькі. Ідеальне переадресація корисна лише для роботи з шаблонами з метою сказати: якщо мені потрібно передати об’єкт іншій функції, мені потрібно, що якщо я отримаю іменований об'єкт, об’єкт передається як названий об'єкт, а коли ні, Я хочу передати його як неназваний об'єкт:
template<typename T>
void some_function(T&& a)
{
other_function(std::forward<T>(a));
}
Ось підпис прототипічної функції, яка використовує ідеальну переадресацію, реалізовану в C ++ 11 засобами std::forward
. Ця функція використовує деякі правила ідентифікації шаблонів:
`A& && == A&`
`A&& && == A&&`
Отже, якщо T
посилання lvalue на A
( T = A &), a
також ( A & && => A &). Якщо посилання T
є реальним значенням A
, a
також (A&&&& => A&&). В обох випадках a
це іменований об'єкт у фактичній області застосування, але T
містить інформацію про його "тип посилання" з точки зору області виклику. Ця інформація ( T
) передається як параметр шаблону до forward
та "a" переміщується або не залежить від типу T
.
Це як семантика копіювання, але замість того, щоб дублювати всі отримані вами дані, щоб вкрасти дані з об’єкта, з якого «переміщено».
Ви знаєте, що означає семантика копії? це означає, що у вас є типи, які можна скопіювати, для визначених користувачем типів ви визначаєте це або купуєте явно записуючий конструктор копій та привласнення оператора, або компілятор генерує їх неявно. Це зробить копію.
Семантика переміщення - це в основному тип, визначений користувачем, з конструктором, який приймає посилання на значення r (новий тип посилання з використанням && (так, два амперсанда)), що не є const, це називається конструктором переміщення, те ж саме стосується оператора призначення. Отже, що робить конструктор переміщення, і замість того, щоб копіювати пам'ять із аргументу джерела, він 'переміщує' пам'ять від джерела до місця призначення.
Коли ви хочете це зробити? well std :: vector - це приклад, скажімо, ви створили тимчасовий std :: vector, і ви повернете його з функції, скажіть:
std::vector<foo> get_foos();
Коли у вас повернеться функція, ви матимете накладні витрати від конструктора копіювання, якщо (і це буде в C ++ 0x) std :: вектор має конструктор переміщення, а не копіювати, він може просто встановити його вказівники та динамічно виділити "переміщення" пам'ять до нового екземпляра. Це схоже на семантику передачі власності з std :: auto_ptr.
Щоб проілюструвати потребу в семантиці переміщення , розглянемо цей приклад без семантики переміщення:
Ось функція, яка приймає об’єкт типу T
і повертає об’єкт одного типу T
:
T f(T o) { return o; }
//^^^ new object constructed
Вищенаведена функція використовує виклик за значенням, що означає, що коли ця функція називається, об'єкт повинен бути побудований для використання функції.
Оскільки функція також повертається за значенням , для повертаного значення будується ще один новий об'єкт:
T b = f(a);
//^ new object constructed
Побудовано два нові об’єкти, один з яких є тимчасовим об’єктом, який використовується лише протягом тривалості функції.
Коли новий об'єкт створюється із поверненого значення, конструктор копії викликається для копіювання вмісту тимчасового об'єкта на новий об'єкт b. Після завершення функції тимчасовий об'єкт, який використовується у функції, виходить із сфери застосування та знищується.
Тепер давайте розглянемо, що робить конструктор копій .
Він повинен спочатку ініціалізувати об’єкт, а потім скопіювати всі відповідні дані зі старого об'єкта на новий.
Залежно від класу, можливо, його контейнер з дуже великою кількістю даних, то це може представляти багато часу та використання пам'яті
// Copy constructor
T::T(T &old) {
copy_data(m_a, old.m_a);
copy_data(m_b, old.m_b);
copy_data(m_c, old.m_c);
}
Завдяки семантиці переміщення тепер можна зробити більшу частину цієї роботи менш неприємною, просто перемістившись дані, а не копіюючи.
// Move constructor
T::T(T &&old) noexcept {
m_a = std::move(old.m_a);
m_b = std::move(old.m_b);
m_c = std::move(old.m_c);
}
Переміщення даних передбачає повторне асоціювання даних із новим об'єктом. І жодна копія не відбувається взагалі.
Це здійснюється за допомогою rvalue
посилання. Посилання працює досить багато , як посилання з однією важливою відмінністю: Rvalue посилання може бути переміщена і іменує не може.rvalue
lvalue
З сайту cppreference.com :
Щоб зробити можливою гарантію винятку, визначені користувачем конструктори переміщення не повинні кидати винятки. Насправді, стандартні контейнери зазвичай покладаються на std :: move_if_noexcept, щоб вибирати між переміщенням і копіюванням, коли елементи контейнера потрібно переселяти. Якщо надані як конструктори копіювання, так і переміщення, роздільна здатність перевантаження вибирає конструктор переміщення, якщо аргумент є rvalue (або prvalue, наприклад, тимчасове ім'я без назви, або xvalue, як результат std :: move), і вибирає конструктор копії, якщо аргумент - це значення lvalue (названий об'єкт або функція / оператор, що повертає посилання lvalue). Якщо надається лише конструктор копій, всі категорії аргументів вибирають її (доки вона посилається на const, оскільки rvalues можуть пов'язувати посилання const), що робить копіювання резервного копіювання для переміщення, коли переміщення недоступне. У багатьох ситуаціях конструктори рухів оптимізуються навіть у тому випадку, якщо вони створюють видимі побічні ефекти, див. Конструктор називається "конструктором переміщення", коли він бере параметр rvalue як параметр. Не потрібно нічого переміщувати, класу не потрібно мати ресурс для переміщення, і «конструктор переміщення» може не мати змоги перемістити ресурс, як у допустимому (але, можливо, не розумному) випадку, коли параметром є посилання const rvalue (const T&&).
Я пишу це, щоб переконатися, що я це правильно зрозумів.
Рух семантика була створена, щоб уникнути зайвого копіювання великих об'єктів. Б'ярн Струструп у своїй книзі "Мова програмування на C ++" використовує два приклади, коли за замовчуванням відбувається непотрібне копіювання: один, заміна двох великих об'єктів та два - повернення великого об'єкта з методу.
Заміна двох великих об'єктів зазвичай включає копіювання першого об'єкта на тимчасовий об'єкт, копіювання другого об'єкта на перший об'єкт та копіювання тимчасового об'єкта на другий об’єкт. Для вбудованого типу це дуже швидко, але для великих об'єктів ці три копії можуть зайняти велику кількість часу. "Присвоєння переміщення" дозволяє програмісту змінити поведінку копіювання за замовчуванням і замість цього поміняти посилання на об'єкти, а це означає, що копіювання взагалі не відбувається і операція підкачки відбувається набагато швидше. Визначення переміщення можна викликати за допомогою виклику методу std :: move ().
Повернення об'єкта з методу за замовчуванням передбачає створення копії локального об’єкта та пов’язаних з ним даних у місці, доступному для абонента (оскільки локальний об’єкт недоступний абоненту і зникає, коли метод закінчується). Коли повертається вбудований тип, ця операція проходить дуже швидко, але якщо повертається великий об'єкт, це може зайняти багато часу. Конструктор переміщення дозволяє програмісту змінити цю поведінку за замовчуванням і замість цього "повторно використати" дані купи, пов'язані з локальним об'єктом, вказуючи на об'єкт, який повертається абоненту, для збирання даних, пов'язаних з локальним об'єктом. Тому копіювання не потрібно.
У мовах, які не дозволяють створювати локальні об'єкти (тобто об'єкти в стеку), такі типи проблем не виникають, оскільки всі об'єкти розподіляються на купі і завжди доступні через посилання.
x
і y
, ви не можете просто «посилання своп на об'єкти» ; можливо, об’єкти містять вказівники, на які посилаються інші дані, і ці покажчики можуть бути помінені, але оператори переміщення нічого не вимагають . Вони можуть видаляти дані з переміщеного об'єкта, а не зберігати в них дані dest.
swap()
без рухомої семантики. "Визначення переміщення можна викликати за допомогою виклику методу std :: move ()." - іноді доводиться використовувати std::move()
- хоча це насправді нічого не переміщує - просто дає змогу компілятору знати, що аргумент є рухомим, іноді std::forward<>()
(з посиланням для переадресації), а в інший раз компілятор знає, що значення можна перемістити.
Ось відповідь із книги "Мова програмування на C ++" Б'ярна Струструпа. Якщо ви не хочете переглянути відео, ви можете переглянути текст нижче:
Розглянемо цей фрагмент. Повернення від оператора + передбачає копіювання результату з локальної змінної res
і кудись там, де абонент може отримати доступ до нього.
Vector operator+(const Vector& a, const Vector& b)
{
if (a.size()!=b.size())
throw Vector_siz e_mismatch{};
Vector res(a.size());
for (int i=0; i!=a.size(); ++i)
res[i]=a[i]+b[i];
return res;
}
Ми не дуже хотіли копії; ми просто хотіли отримати результат з функції. Тому нам потрібно перемістити вектор, а не копіювати його. Ми можемо визначити конструктор переміщення таким чином:
class Vector {
// ...
Vector(const Vector& a); // copy constructor
Vector& operator=(const Vector& a); // copy assignment
Vector(Vector&& a); // move constructor
Vector& operator=(Vector&& a); // move assignment
};
Vector::Vector(Vector&& a)
:elem{a.elem}, // "grab the elements" from a
sz{a.sz}
{
a.elem = nullptr; // now a has no elements
a.sz = 0;
}
&& означає "посилання на rvalue" і є посиланням, на яке ми можемо прив'язати rvalue. "rvalue" 'призначений для доповнення "lvalue", що приблизно означає "те, що може з'явитися з лівої сторони завдання". Отже, rvalue означає приблизно "значення, яке ви не можете призначити", наприклад, ціле число, повернене викликом функції, і res
локальна змінна в операторі + () для Vectors.
Тепер заяву return res;
не копіювати!