Що таке std :: move (), і коли його слід використовувати?


656
  1. Що це?
  2. Що це робить?
  3. Коли його слід використовувати?

Гарні посилання цінуються.


43
Bjarne Stroustrup пояснює хід у короткому вступі до рейтингів Rvalue
DumbCoder


12
Це питання стосується std::move(T && t); також існує std::move(InputIt first, InputIt last, OutputIt d_first)алгоритм, пов'язаний з цим std::copy. Я наголошую на цьому, щоб інші не такі розгублені, як я, коли вперше зіткнулися з std::moveприйняттям трьох аргументів. en.cppreference.com/w/cpp/algorithm/move
josaphatv

Відповіді:


287

Сторінка Вікіпедії на C ++ 11 посилання на значення R та конструктори переміщення

  1. У C ++ 11, крім копіюючих конструкторів, об’єкти можуть мати конструктори переміщення.
    (Окрім операторів копіювання, вони також мають операторів присвоєння переміщення.)
  2. Конструктор переміщення використовується замість конструктора копіювання, якщо об’єкт має тип "rvalue-reference" ( Type &&).
  3. std::move() являє собою команду, яка виробляє rvalue-посилання на об'єкт, щоб забезпечити рух від нього.

Це новий спосіб C ++, щоб уникнути копій. Наприклад, використовуючи конструктор переміщення, a std::vectorможе просто скопіювати свій внутрішній вказівник на дані на новий об’єкт, залишивши переміщений об’єкт у переміщеному стані, тому не копіюючи всіх даних. Це буде C ++ - дійсно.

Спробуйте googling для семантики переміщення, оцінок, ідеального переадресації.


40
Семантика переміщення вимагає, щоб переміщений об'єкт залишався дійсним , що не є неправильним станом. (Обгрунтування: його все одно потрібно знищити, змусити його працювати.)
GManNickG

13
@GMan: ну, це повинно бути в такому стані, яке можна зруйнувати, але, AFAIK, воно не повинно бути корисним для нічого іншого.
Zan Lynx

8
@ZanLynx: Правильно. Зауважте, що стандартна бібліотека додатково вимагає присвоєння переміщених об'єктів, але це лише для об'єктів, що використовуються в stdlib, а не загальної вимоги.
GManNickG

25
-1 "std :: move () - це C ++ 11 спосіб використання семантики переміщення" Будь ласка, виправте це. std::move()не спосіб використання семантики переміщення, семантика переміщення виконується прозоро для програміста. moveйого єдиний склад для передачі значення з однієї точки в іншу, де початкове значення lvalue більше не буде використовуватися.
Manu343726

15
Я б пішов далі. std::moveсама по собі нічого не робить - вона має нульові побічні ефекти. Це просто сигналізує компілятору, що програмісту вже не байдуже, що відбувається з цим об’єктом. тобто він дає дозвіл іншим частинам програмного забезпечення переходити від об'єкта, але не вимагає його переміщення. Насправді, одержувач посилання на рецензію не повинен обіцяти, що він буде чи не буде робити з даними.
Аарон Мак-Дейд

241

1. "Що це?"

Хоча std::move() технічно це функція - я б сказав, що це насправді не функція . Це свого роду перетворювач між способами компілятор вважає значення виразу.

2. "Що це робить?"

Перше, що слід зазначити, це те, std::move() що насправді нічого не рухає . Він перетворює вираз із значення lvalue (наприклад, названої змінної) у xvalue . Значення xvalue повідомляє компілятору:

Ви можете пограбувати мене, перемістити все, що я тримаю, і використати його в іншому місці (оскільки я все одно незабаром буду знищений) ".

Іншими словами, коли ви використовуєте std::move(x), ви дозволяєте компілятору канібалізувати x. Таким чином, якщо x, скажімо, є власний буфер в пам'яті - після std::move()того, як компілятор може мати власний інший об'єкт.

Ви також можете перейти від першого значення (наприклад, тимчасового, коли ви проїжджаєте), але це рідко корисно.

3. "Коли його слід використовувати?"

Ще один спосіб задати це питання: "Для чого я можу канібалізувати ресурси існуючого об'єкта?" добре, якщо ви пишете код програми, ви, ймовірно, не будете багато возитися з тимчасовими об'єктами, створеними компілятором. Тому в основному ви робите це в таких місцях, як конструктори, операторські методи, функції, подібні до стандартної бібліотеки та алгоритму тощо. Звичайно, це просто велике правило.

Типовим є «переміщення» ресурсів від одного об’єкта до іншого замість копіювання. @Guillaume посилається на цю сторінку, яка має простий короткий приклад: обмін двома об'єктами з меншим копіюванням.

template <class T>
swap(T& a, T& b) {
    T tmp(a);   // we now have two copies of a
    a = b;      // we now have two copies of b (+ discarded a copy of a)
    b = tmp;    // we now have two copies of tmp (+ discarded a copy of b)
}

використання переміщення дозволяє обмінюватися ресурсами, а не копіювати їх навколо:

template <class T>
swap(T& a, T& b) {
    T tmp(std::move(a));
    a = std::move(b);   
    b = std::move(tmp);
}

Подумайте, що відбувається, коли T, скажімо, vector<int>розміру n. У першій версії ви читаєте і записуєте 3 * n елементів, у другій версії ви в основному читаєте і записуєте лише 3 вказівника до буферів векторів плюс 3 розміри буферів. Звичайно, класу Tпотрібно знати, як робити рухомий; ваш клас повинен мати оператора присвоєння переміщення та конструктора переміщення для класу, Tщоб це працювало.


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

7
@TitoneMaurice: За винятком того, що це не копія - оскільки оригінальне значення вже не використовується.
einpoklum

3
@Zebrafish ти не можеш більше помилитися. Неглибока копія залишає оригінал у точно такому ж стані. Зазвичай переміщення призводить до того, що оригінал порожній або в іншому випадку дійсний.
rubenvb

16
@rubenvb Zebra не зовсім помиляється. Хоча це правда, що оригінальний каналізований об'єкт, як правило, навмисно саботується, щоб уникнути плутаних помилок (наприклад, встановити його покажчики на nullptr, щоб сигналізувати, що йому більше не належать пуанти), факт, що весь хід реалізується шляхом простого копіювання покажчика з джерела до місця призначення (і навмисне уникати будь-яких дій з покажчиком) справді нагадує дрібну копію. Насправді я б пішов так далеко, щоб сказати, що цей крок - це неглибока копія з подальшим необов'язковим частковим самознищенням джерела. (продовження)
Гонки легкості на орбіті

3
(продовження.) Якщо ми дозволяємо це визначення (і мені це більше подобається), то спостереження @ Зебрафіш не є помилковим, просто трохи неповним.
Гонки легкості по орбіті

145

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

Це посилання дійсно допомогло мені:

http://thbecker.net/articles/rvalue_references/section_01.html

Вибачте, якщо моя відповідь надходить занадто пізно, але я також шукав хорошого посилання для std :: move, і я знайшов посилання вище трохи "суворими".

Це робить акцент на r-значення посилання, у якому контексті ви повинні їх використовувати, і я думаю, що це більш детально, тому я хотів поділитися цим посиланням тут.


26
Приємне посилання. Я завжди знаходив статтю у Вікіпедії та інші посилання, на які я натрапив, доволі заплутаний, оскільки вони просто кидають на вас факти, залишаючи це вам, щоб зрозуміти, що таке власне значення / обґрунтування. Хоча "переміщення семантики" в конструкторі досить очевидно, всі ці подробиці про передачу & & - значень навколо не є ... тому опис стилю підручника був дуже приємним.
Крістіан Стібер

66

З: Що таке std::move?

A: std::move()це функція зі стандартної бібліотеки C ++ для лиття на посилання rvalue.

Спрощено std::move(t)рівнозначно:

static_cast<T&&>(t);

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

int a = 3; // 3 is a rvalue, does not exist after expression is evaluated
int b = a; // a is a lvalue, keeps existing after expression is evaluated

Реалізація для std :: move () наведена в N2027: "Короткий вступ до посилання Rvalue" наступним чином:

template <class T>
typename remove_reference<T>::type&&
std::move(T&& a)
{
    return a;
}

Як бачите, std::moveповертаєтьсяT&& значення незалежно від того, якщо його викликають зі значенням ( T), типом посилання ( T&) або посиланням на rvalue ( T&&).

З: Що це робить?

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

foo(3 * 5); // obviously, you are calling foo with a temporary (rvalue)

int a = 3 * 5;
foo(a);     // how to tell the compiler to treat `a` as an rvalue?
foo(std::move(a)); // will call `foo(int&& a)` rather than `foo(int a)` or `foo(int& a)`

Що це робить не робить:

  • Зробіть копію аргументу
  • Викличте конструктор копій
  • Змініть об’єкт аргументу

Питання: Коли його слід використовувати?

A: Ви повинні використовувати std::move якщо ви хочете викликати функції, які підтримують семантику переміщення, аргументом, який не є rvalue (тимчасовим виразом).

Це вимагає наступних питань для мене:

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

  • Які класи та об'єкти підтримують семантику переміщення? Ви, як розробник, вирішувати впроваджувати семантику переміщення у своїх власних класах, якщо вони отримають користь від передачі своїх членів, а не копіювання. Після того як ви реалізуєте семантику переміщення, ви отримаєте безпосередню користь від роботи багатьох програмістів бібліотеки, які додали підтримку для ефективного керування класами з семантикою переміщення.

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

  • У яких ситуаціях я хотів би сказати компілятору, що він повинен ставитися до змінної як до rvalue? Найімовірніше, це відбудеться в функціях шаблону або бібліотеки, де ви знаєте, що проміжний результат може бути виправлений


2
Великий +1 для прикладів коду з семантикою в коментарях. Інші головні відповіді визначають std :: move, використовуючи сам "move" - ​​насправді нічого не з'ясовує! --- Я вважаю, що варто згадати, що не робити копію аргументу означає, що вихідне значення не може бути надійно використане.
друк

34

std :: сам переміщення насправді не дуже багато. Я подумав, що він називає переміщений конструктор для об'єкта, але він справді просто виконує передачу типу (кидає змінну lvalue в rvalue, щоб згадана змінна могла бути передана як аргумент конструктору переміщення або оператору призначення).

Отже, std :: move просто використовується як попередник використання семантики переміщення. Семантика переміщення - це, по суті, ефективний спосіб поводження з тимчасовими об'єктами.

Розглянемо об’єкт A = B + C + D + E + F;

Це добре виглядає код, але E + F створює тимчасовий об'єкт. Тоді D + temp виробляє ще один тимчасовий об'єкт тощо. У кожному звичайному "+" операторі класу трапляються глибокі копії.

Наприклад

Object Object::operator+ (const Object& rhs) {
    Object temp (*this);
    // logic for adding
    return temp;
}

Створення тимчасового об’єкта в цій функції марно - ці тимчасові об'єкти все одно будуть видалені в кінці рядка, коли вони вийдуть за межі області.

Ми можемо скоріше використовувати семантику переміщення, щоб "грабувати" тимчасові об'єкти і робити щось подібне

 Object& Object::operator+ (Object&& rhs) {
     // logic to modify rhs directly
     return rhs;
 }

Це дозволяє уникнути зайвих глибоких копій. Посилаючись на приклад, єдиною частиною, де відбувається глибоке копіювання, є тепер E + F. Решта використовує семантику переміщення. Конструктор переміщення або оператор призначення також повинен бути реалізований для присвоєння результату А.


3
ви говорили про семантику руху. ви маєте додати до своєї відповіді як те, як може використовуватися std :: move, оскільки питання задається цим питанням.
Кушик Шетті

2
@Koushik std :: move не робить багато - але використовується для реалізації семантики переміщення. Якщо ви не знаєте про std :: move, ви, ймовірно, не знаєте і семантики переміщення
user929404

1
"не роби багато" (так, просто static_cast для посилання на оцінку). що насправді це робить, і що він робить, це те, що запитувала ОП. вам не потрібно знати, як працює std :: move, але ви повинні знати, що робить семантика переміщення. крім того, "але використовується для реалізації семантики переміщення" його навпаки. знаю, переміщуємо семантику, і ти зрозумієш std :: переміститися інакше ні. рух просто допомагає в русі і сам використовує семантику руху. std :: move не робить нічого, крім перетворення свого аргументу в рецензує посилання, для чого потрібна семантика переміщення.
Кушик Шетті

10
"але E + F створює тимчасовий об'єкт" - Оператор +йде вліво-вправо, а не вправо-вліво. Отже, B+Cбуло б першим!
Аджай

8

"Що це?" і "Що це робить?" було пояснено вище.

Наведу приклад "коли це слід використовувати".

Наприклад, у нас є клас з великою кількістю ресурсів, таких як великий масив.

class ResHeavy{ //  ResHeavy means heavy resource
    public:
        ResHeavy(int len=10):_upInt(new int[len]),_len(len){
            cout<<"default ctor"<<endl;
        }

        ResHeavy(const ResHeavy& rhs):_upInt(new int[rhs._len]),_len(rhs._len){
            cout<<"copy ctor"<<endl;
        }

        ResHeavy& operator=(const ResHeavy& rhs){
            _upInt.reset(new int[rhs._len]);
            _len = rhs._len;
            cout<<"operator= ctor"<<endl;
        }

        ResHeavy(ResHeavy&& rhs){
            _upInt = std::move(rhs._upInt);
            _len = rhs._len;
            rhs._len = 0;
            cout<<"move ctor"<<endl;
        }

    // check array valid
    bool is_up_valid(){
        return _upInt != nullptr;
    }

    private:
        std::unique_ptr<int[]> _upInt; // heavy array resource
        int _len; // length of int array
};

Код тесту:

void test_std_move2(){
    ResHeavy rh; // only one int[]
    // operator rh

    // after some operator of rh, it becomes no-use
    // transform it to other object
    ResHeavy rh2 = std::move(rh); // rh becomes invalid

    // show rh, rh2 it valid
    if(rh.is_up_valid())
        cout<<"rh valid"<<endl;
    else
        cout<<"rh invalid"<<endl;

    if(rh2.is_up_valid())
        cout<<"rh2 valid"<<endl;
    else
        cout<<"rh2 invalid"<<endl;

    // new ResHeavy object, created by copy ctor
    ResHeavy rh3(rh2);  // two copy of int[]

    if(rh3.is_up_valid())
        cout<<"rh3 valid"<<endl;
    else
        cout<<"rh3 invalid"<<endl;
}

вихід, як показано нижче:

default ctor
move ctor
rh invalid
rh2 valid
copy ctor
rh3 valid

Ми можемо бачити, що std::moveз допомогою move constructorперетворення ресурсу легко.

Де ще std::moveкорисно?

std::moveтакож може бути корисним при сортуванні масиву елементів. Багато алгоритмів сортування (наприклад, сортування сортування та сортування міхурів) працюють шляхом заміни пар елементів. Раніше нам довелося вдатися до копіювання-семантики, щоб зробити своп. Тепер ми можемо використовувати семантику переміщення, яка є більш ефективною.

Це також може бути корисно, якщо ми хочемо перемістити вміст, керований одним розумним вказівником, на інший.

Цитується:


0

Ось повний приклад, використовуючи std :: move для (простого) користувацького вектора

Очікуваний вихід:

 c: [10][11]
 copy ctor called
 copy of c: [10][11]
 move ctor called
 moved c: [10][11]

Складіть як:

  g++ -std=c++2a -O2 -Wall -pedantic foo.cpp

Код:

#include <iostream>
#include <algorithm>

template<class T> class MyVector {
private:
    T *data;
    size_t maxlen;
    size_t currlen;
public:
    MyVector<T> () : data (nullptr), maxlen(0), currlen(0) { }
    MyVector<T> (int maxlen) : data (new T [maxlen]), maxlen(maxlen), currlen(0) { }

    MyVector<T> (const MyVector& o) {
        std::cout << "copy ctor called" << std::endl;
        data = new T [o.maxlen];
        maxlen = o.maxlen;
        currlen = o.currlen;
        std::copy(o.data, o.data + o.maxlen, data);
    }

    MyVector<T> (const MyVector<T>&& o) {
        std::cout << "move ctor called" << std::endl;
        data = o.data;
        maxlen = o.maxlen;
        currlen = o.currlen;
    }

    void push_back (const T& i) {
        if (currlen >= maxlen) {
            maxlen *= 2;
            auto newdata = new T [maxlen];
            std::copy(data, data + currlen, newdata);
            if (data) {
                delete[] data;
            }
            data = newdata;
        }
        data[currlen++] = i;
    }

    friend std::ostream& operator<<(std::ostream &os, const MyVector<T>& o) {
        auto s = o.data;
        auto e = o.data + o.currlen;;
        while (s < e) {
            os << "[" << *s << "]";
            s++;
        }
        return os;
    }
};

int main() {
    auto c = new MyVector<int>(1);
    c->push_back(10);
    c->push_back(11);
    std::cout << "c: " << *c << std::endl;
    auto d = *c;
    std::cout << "copy of c: " << d << std::endl;
    auto e = std::move(*c);
    delete c;
    std::cout << "moved c: " << e << std::endl;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.