Що таке семантика руху?


1701

Я щойно закінчив слухати інтерв'ю радіо подкастів Software Engineering з Скоттом Майєрсом щодо C ++ 0x . Більшість нових функцій мали для мене сенс, і я зараз захоплююсь C ++ 0x, за винятком однієї. Я все ще не отримую семантики руху ... Що це саме?


20
Я знайшов [статтю в блозі Елі Бендерського] ( eli.thegreenplace.net/2011/12/15/… ) про значення та rvalues ​​у C та C ++ досить інформативно. Він також згадує посилання на оцінку в C ++ 11 і представляє їх з невеликими прикладами.
Нілс


19
Щороку або близько того мені цікаво, про що йдеться про "нову" семантику переміщення в C ++, я переглядаю її в Google і переходжу на цю сторінку. Я читаю відповіді, мій мозок відключається. Я повертаюся до С, і все забуваю! Я тупик.
небо

7
@sky Розгляньте std :: vector <> ... Десь там вказівник на масив на купі. Якщо ви скопіюєте цей об'єкт, потрібно виділити новий буфер, а дані з буфера потрібно скопіювати в новий буфер. Чи є якась обставина, коли було б добре просто вкрасти покажчик? Відповідь ТАК, коли компілятор знає, що об'єкт тимчасовий. Семантика переміщення дозволяє вам визначити, як кишки ваших класів можуть бути переміщені та скинуті в інший об’єкт, коли компілятор знає, що об’єкт, з якого ви рухаєтесь, збирається піти.
dicroce

Єдине посилання, яке я можу зрозуміти: learncpp.com/cpp-tutorial/… , тобто оригінальне міркування семантики переміщення - з розумних покажчиків.
jw_

Відповіді:


2479

Мені найпростіше зрозуміти переміщення семантики з прикладом коду. Почнемо з дуже простого рядкового класу, який містить лише вказівник на виділений в пам'ять блок пам'яті:

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

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

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


40
@ Але якщо мій реєстр отримує оцінку, яку більше не можна використовувати пізніше, чому мені взагалі потрібно турбуватися, щоб залишити його в стабільному / безпечному стані? Замість того, щоб встановити that.data = 0, чому б просто не залишити його?
einpoklum

70
@einpoklum Тому що без that.data = 0цього персонажів було б знищено занадто рано (коли тимчасові помирають), а також двічі. Ви хочете вкрасти дані, а не ділитися ними!
fredoverflow

19
@einpoklum Запланований деструктор все-таки запускається, тому ви повинні переконатися, що стан після переміщення об'єкта-джерела не спричинить збій. Краще, ви повинні переконатися, що вихідним об’єктом також може бути приймач завдання або іншого запису.
CTMacUser

12
@pranitkothari Так, всі об'єкти повинні бути знищені, навіть переміщені з об'єктів. І оскільки ми не хочемо, щоб масив char видалявся, коли це трапляється, ми мусимо встановити покажчик на null.
fredoverflow

7
@ Virus721 delete[]на nullptr визначається стандартом C ++, щоб бути неоперативним.
fredoverflow

1057

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

Стефан Т. Лававей знайшов час, щоб надати цінні відгуки. Дуже дякую, Стефане!

Вступ

Семантика переміщення дозволяє об’єкту за певних умов брати право власності на зовнішні ресурси якогось іншого об'єкта. Це важливо двома способами:

  1. Перетворення дорогих копій в дешеві рухи. Дивіться для прикладу мою першу відповідь. Зауважте, що якщо об’єкт не керує хоча б одним зовнішнім ресурсом (безпосередньо чи опосередковано через об'єкти його учасника), семантика переміщення не надасть жодних переваг перед семантикою копіювання. У такому випадку копіювання об'єкта та переміщення об'єкта означає саме те саме:

    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
    
        // ...
    };
  2. Впровадження безпечних типів "лише для переміщення"; тобто типи, для яких копіювання не має сенсу, але переміщення робить. Приклади включають блокування, ручки файлів та розумні покажчики з унікальною семантикою власності. Примітка. У цій відповіді обговорюється std::auto_ptrзастарілий стандартний шаблон бібліотеки C ++ 98, який був замінений на std::unique_ptrC ++ 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 ​​типів класів з оператором призначення).

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

Rvalue посилання

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

Xvalues

Зауважте, що хоч 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не позначає автоматичний об'єкт, а замість цього об'єкт, переданий абонентом.



24
Є третя причина, що важлива семантика переміщення: безпека виключень. Часто, коли операція копіювання може кидати (тому що їй потрібно виділити ресурси, а розподіл може бути невдалим) операція переміщення може бути безвідмовною (оскільки вона може передати право власності на існуючі ресурси замість виділення нових). Операції, які не можуть завершитись, завжди приємно, і це може бути вирішальним при написанні коду, який забезпечує гарантії виключення.
Брангдон

8
Я був з вами аж до "Універсальних посилань", але тоді це все занадто абстрактно. Довідник згортається? Ідеальне переадресація? Ви хочете сказати, що посилання rvalue стає універсальною посиланням, якщо тип шаблонується? Мені б хотілося, щоб це було пояснити, щоб я знав, потрібно мені це зрозуміти чи ні! :)
Kylotan

8
Будь ласка, напишіть книгу зараз ... ця відповідь дала мені підстави вірити, якщо ви висвітлюєте інші куточки C ++ так легко, як це, ще тисячі людей зрозуміють це.
halivingston

12
@halivingston Дуже дякую за добрі відгуки, я дуже ціную це. Проблема з написанням книги полягає в тому, що це набагато більше роботи, ніж ви можете собі уявити. Якщо ви хочете заглибитися в C ++ 11 і більше, пропоную вам придбати "Ефективний сучасний C ++" Скотта Майєрса.
fredoverflow

77

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


1
зауважте, що це має бути this->swap(std::move(rhs));тому, що названі посилання rvalue
wmamrak

Це якось неправильно, за коментарем @ Tacyt: rhsце значення в контексті X::X(X&& rhs). Вам потрібно зателефонувати, std::move(rhs)щоб отримати оцінку, але ця ситуація робить відповідь суперечкою.
Ашера

Що означає переміщення семантики для типів без покажчиків? Перемістити семантику творів примірника?
Гусєв Слава

@ Гусєв: Я поняття не маю, про що ви питаєте.
sbi

60

Припустимо, у вас є функція, яка повертає істотний об'єкт:

Matrix multiply(const Matrix &a, const Matrix &b);

Коли ви пишете такий код:

Matrix r = multiply(a, b);

тоді звичайний компілятор C ++ створить тимчасовий об’єкт для результату multiply(), викличе конструктор копій для ініціалізації rта знищить тимчасове повернене значення. Семантика переміщення в C ++ 0x дозволяє викликати "конструктор переміщення" для ініціалізаціїr , копіюючи його вміст, а потім відкидає тимчасове значення, не руйнуючи його.

Це особливо важливо, якщо (як, можливо, Matrixприклад вище) об'єкт, який копіюється, виділяє додаткову пам’ять на купі для зберігання його внутрішнього подання. Конструктору копій доведеться або зробити повну копію внутрішнього представлення, або використовувати семантику підрахунку посилань та семантику копіювання на запис цілком. Конструктор переміщення залишив би пам'ять купи в спокої і просто скопіював покажчик всередині Matrixоб'єкта.


2
Чим відрізняються конструктори переміщення та конструктори копій?
dicroce

1
@dicroce: Вони відрізняються за синтаксисом, один виглядає як Matrix (const Matrix & src) (конструктор копій), а інший схожий на Matrix (Matrix && src) (конструктор переміщення), перевірте головну відповідь для кращого прикладу.
snk_kid

3
@dicroce: один робить порожній об’єкт, а один робить копію. Якщо дані, що зберігаються в об'єкті, великі, копія може дорого коштувати. Наприклад, std :: вектор.
Біллі ONeal

1
@ kunj2aan: Підозрюю, це залежить від вашого компілятора. Компілятор може створити тимчасовий об'єкт всередині функції, а потім перемістити його у зворотне значення абонента. Або, можливо, він зможе безпосередньо сконструювати об’єкт у зворотному значенні, не потребуючи використання конструктора переміщення.
Грег Хьюгілл

2
@Jichao: Це оптимізація називається РВО, см це питання для отримання додаткової інформації про відмінності: stackoverflow.com/questions/5031778 / ...
Greg Hewgill

30

Якщо ви дійсно зацікавлені в хорошому, глибокому поясненні семантики переміщення, я настійно рекомендую прочитати на них оригінальний документ "Пропозиція додати підтримку Move Semantics до мови C ++".

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


27

Семантика руху - це передача ресурсів, а не їх копіювання, коли вже нікому не потрібне значення джерела.

У C ++ 03 об'єкти часто копіюються, лише їх знищують або передають перед тим, як будь-який код знову використовує це значення. Наприклад, коли ви повертаєтесь за значенням з функції - якщо RVO не запускається - значення, яке ви повертаєте, копіюється в кадр стека абонента, а потім воно виходить за межі і знищується. Це лише один із багатьох прикладів: див. Прохідне значення, коли вихідний об'єкт є тимчасовим, такі алгоритми sortпросто переставляють елементи, перерозподіляють, vectorколи його capacity()перевищують тощо.

Коли такі копіювати / знищувати пари дорого, це зазвичай тому, що об'єкт володіє деяким важким ресурсом. Наприклад, vector<string>може бути власник динамічно виділеного блоку пам'яті, що містить масив stringоб'єктів, кожен з яких має власну динамічну пам'ять. Копіювання такого об’єкта коштує дорого: ви повинні виділити нову пам'ять для кожного динамічно виділених блоків у джерелі та скопіювати всі значення впоперек. Тоді вам потрібно розібрати всю пам’ять, яку ви щойно скопіювали. Однак переміщення великого vector<string>означає просто скопіювати кілька покажчиків (які відносяться до блоку динамічної пам'яті) до місця призначення та нулювати їх у джерело.


23

Простим (практичним) терміном:

Копіювання об'єкта означає копіювання його "статичних" членів та виклик 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.


20

Це як семантика копіювання, але замість того, щоб дублювати всі отримані вами дані, щоб вкрасти дані з об’єкта, з якого «переміщено».


13

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

Семантика переміщення - це в основному тип, визначений користувачем, з конструктором, який приймає посилання на значення r (новий тип посилання з використанням && (так, два амперсанда)), що не є const, це називається конструктором переміщення, те ж саме стосується оператора призначення. Отже, що робить конструктор переміщення, і замість того, щоб копіювати пам'ять із аргументу джерела, він 'переміщує' пам'ять від джерела до місця призначення.

Коли ви хочете це зробити? well std :: vector - це приклад, скажімо, ви створили тимчасовий std :: vector, і ви повернете його з функції, скажіть:

std::vector<foo> get_foos();

Коли у вас повернеться функція, ви матимете накладні витрати від конструктора копіювання, якщо (і це буде в C ++ 0x) std :: вектор має конструктор переміщення, а не копіювати, він може просто встановити його вказівники та динамічно виділити "переміщення" пам'ять до нового екземпляра. Це схоже на семантику передачі власності з std :: auto_ptr.


1
Я не думаю, що це чудовий приклад, тому що в цих прикладах функцій повернення значення Оптимізація поверненого значення, ймовірно, вже виключає операцію копіювання.
Zan Lynx

7

Щоб проілюструвати потребу в семантиці переміщення , розглянемо цей приклад без семантики переміщення:

Ось функція, яка приймає об’єкт типу 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 посилання може бути переміщена і іменує не може.
rvaluelvalue

З сайту cppreference.com :

Щоб зробити можливою гарантію винятку, визначені користувачем конструктори переміщення не повинні кидати винятки. Насправді, стандартні контейнери зазвичай покладаються на std :: move_if_noexcept, щоб вибирати між переміщенням і копіюванням, коли елементи контейнера потрібно переселяти. Якщо надані як конструктори копіювання, так і переміщення, роздільна здатність перевантаження вибирає конструктор переміщення, якщо аргумент є rvalue (або prvalue, наприклад, тимчасове ім'я без назви, або xvalue, як результат std :: move), і вибирає конструктор копії, якщо аргумент - це значення lvalue (названий об'єкт або функція / оператор, що повертає посилання lvalue). Якщо надається лише конструктор копій, всі категорії аргументів вибирають її (доки вона посилається на const, оскільки rvalues ​​можуть пов'язувати посилання const), що робить копіювання резервного копіювання для переміщення, коли переміщення недоступне. У багатьох ситуаціях конструктори рухів оптимізуються навіть у тому випадку, якщо вони створюють видимі побічні ефекти, див. Конструктор називається "конструктором переміщення", коли він бере параметр rvalue як параметр. Не потрібно нічого переміщувати, класу не потрібно мати ресурс для переміщення, і «конструктор переміщення» може не мати змоги перемістити ресурс, як у допустимому (але, можливо, не розумному) випадку, коли параметром є посилання const rvalue (const T&&).


7

Я пишу це, щоб переконатися, що я це правильно зрозумів.

Рух семантика була створена, щоб уникнути зайвого копіювання великих об'єктів. Б'ярн Струструп у своїй книзі "Мова програмування на C ++" використовує два приклади, коли за замовчуванням відбувається непотрібне копіювання: один, заміна двох великих об'єктів та два - повернення великого об'єкта з методу.

Заміна двох великих об'єктів зазвичай включає копіювання першого об'єкта на тимчасовий об'єкт, копіювання другого об'єкта на перший об'єкт та копіювання тимчасового об'єкта на другий об’єкт. Для вбудованого типу це дуже швидко, але для великих об'єктів ці три копії можуть зайняти велику кількість часу. "Присвоєння переміщення" дозволяє програмісту змінити поведінку копіювання за замовчуванням і замість цього поміняти посилання на об'єкти, а це означає, що копіювання взагалі не відбувається і операція підкачки відбувається набагато швидше. Визначення переміщення можна викликати за допомогою виклику методу std :: move ().

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

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


"Призначення переміщення" дозволяє програмісту змінити поведінку копіювання за замовчуванням і замість цього поміняти посилання на об'єкти, а це означає, що копіювання взагалі не відбувається і операція підкачки відбувається набагато швидше. " - ці вимоги неоднозначні та оманливі. Щоб поміняти місцями два об'єкти xі y, ви не можете просто «посилання своп на об'єкти» ; можливо, об’єкти містять вказівники, на які посилаються інші дані, і ці покажчики можуть бути помінені, але оператори переміщення нічого не вимагають . Вони можуть видаляти дані з переміщеного об'єкта, а не зберігати в них дані dest.
Тоні Делрой

Можна писати swap()без рухомої семантики. "Визначення переміщення можна викликати за допомогою виклику методу std :: move ()." - іноді доводиться використовувати std::move()- хоча це насправді нічого не переміщує - просто дає змогу компілятору знати, що аргумент є рухомим, іноді std::forward<>()(з посиланням для переадресації), а в інший раз компілятор знає, що значення можна перемістити.
Тоні Делрой

-2

Ось відповідь із книги "Мова програмування на 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;не копіювати!

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