Что такое std :: move () и когда его следует использовать?

657
  1. Что это?
  2. Что оно делает?
  3. Когда его следует использовать?

Хорошие ссылки приветствуются.

Basilevs
источник
43
Бьярн Страуструп объясняет движение в Кратком введении в Rvalue Ссылки
DumbCoder
12
Этот вопрос относится к std::move(T && t); также существует std::move(InputIt first, InputIt last, OutputIt d_first)алгоритм, связанный с std::copy. Я подчеркиваю это, чтобы другие не были так смущены, как я, когда впервые столкнулись с std::moveпринятием трех аргументов. ru.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 ++ - допустимо.

Попробуйте прибегнуть к поиску семантики перемещения, значения, идеальной пересылки.

Scharron
источник
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Сам по себе "ничего" не имеет - у него нулевые побочные эффекты. Это просто сигнализирует компилятору, что программисту уже все равно, что происходит с этим объектом. то есть он дает разрешение другим частям программного обеспечения перемещаться от объекта, но не требует его перемещения. Фактически, получатель ссылки rvalue не должен давать никаких обещаний относительно того, что он будет или не будет делать с данными.
Аарон МакДейд
241

1. «Что это?»

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

2. «Что это делает?»

Первое, что нужно отметить, это то, std::move() что на самом деле ничего не двигается . Он преобразует выражение из lvalue (например, именованной переменной) в xvalue . Значение x сообщает компилятору:

Вы можете разграбить меня, переместить все, что я держу, и использовать это в другом месте (так как я все равно скоро буду уничтожен) ".

другими словами, когда вы используете 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)
}

Использование Move позволяет вам обмениваться ресурсами, а не копировать их:

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чтобы это работало.

einpoklum
источник
3
Долгое время я слышал об этой семантике перемещения, я никогда не изучал их. Из приведенного вами описания кажется, что это просто мелкая копия вместо глубокой.
Зебрафиш
7
@TitoneMaurice: за исключением того, что это не копия - исходное значение больше не может быть использовано.
einpoklum
3
@ Зебрафиш, ты не ошибаешься. Мелкая копия оставляет оригинал в том же состоянии, перемещение обычно приводит к тому, что оригинал будет пустым или в другом действительном состоянии.
rubenvb
17
@rubenvb Зебра не совсем не права. Хотя верно, что исходный каннабилизированный объект обычно намеренно саботируют, чтобы избежать путаницы в ошибках (например, установите его указатели на nullptr, чтобы сигнализировать, что он больше не владеет пуантами), тот факт, что весь ход реализуется простым копированием указателя из источника до места назначения (и намеренно избегать делать что-либо с pointee) действительно напоминает мелкую копию. На самом деле, я бы зашел так далеко, что сказал бы, что ход - это мелкая копия, за которой, возможно, следует частичное самоуничтожение источника. (продолжение)
Гонки
3
(продолжение) Если мы допустим это определение (и мне оно очень нравится), то наблюдение @ Zebrafish не ошибочно, а лишь слегка неполно.
Гонки
146

Вы можете использовать перемещение, когда вам нужно «перенести» содержимое объекта куда-то еще, не делая копию (т. Е. Содержимое не дублируется, поэтому его можно использовать для некоторых не копируемых объектов, таких как unique_ptr). Для объекта также возможно получить содержимое временного объекта без копирования (и сэкономить много времени) с помощью std :: move.

Эта ссылка действительно помогла мне:

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

Я извиняюсь, если мой ответ приходит слишком поздно, но я также искал хорошую ссылку для std :: move, и я нашел ссылки выше немного "строгими".

Это делает упор на ссылку на r-значение, в каком контексте вы должны их использовать, и я думаю, что это более подробно, поэтому я хотел поделиться этой ссылкой здесь.

Гийом
источник
26
Хорошая ссылка. Я всегда находил статью в Википедии и другие ссылки, которые наткнулся на меня, довольно запутанными, поскольку они просто бросают в вас факты, предоставляя вам возможность выяснить, каково реальное значение / обоснование. Хотя «семантика перемещения» в конструкторе довольно очевидна, все эти детали о передаче && - значений не являются ... так что описание в стиле учебника было очень хорошим.
Кристиан Штибер
66

Q: что это std::move?

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

Упрощенно std::move(t)эквивалентно:

static_cast<T&&>(t);

Значение r - это временное значение, которое не сохраняется за пределами определяющего его выражения, например, промежуточный результат функции, который никогда не сохраняется в переменной.

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 References» следующим образом:

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

Как видите, std::moveвозвращает T&&независимо от того, вызывается ли со значением ( T), ссылочным типом ( T&) или rvalue reference ( T&&).

Q: Что это делает?

A: Как актерский состав, он ничего не делает во время выполнения. Во время компиляции уместно сообщить компилятору, что вы хотели бы продолжить рассматривать ссылку как значение.

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)`

Что он не делает:

  • Сделайте копию аргумента
  • Вызовите конструктор копирования
  • Изменить аргумент объекта

Q: Когда это должно использоваться?

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

Это вызывает у меня следующие вопросы:

  • Что такое семантика перемещения? Перемещение семантики в отличие от семантики копирования - это метод программирования, в котором члены объекта инициализируются путем «захвата» вместо копирования элементов другого объекта. Такое «поглощение» имеет смысл только с указателями и дескрипторами ресурсов, которые могут быть легко перенесены путем копирования указателя или целочисленного дескриптора, а не базовых данных.

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

  • Почему компилятор не может понять это самостоятельно? Компилятор не может просто вызвать другую перегрузку функции, если вы не скажете так. Вы должны помочь компилятору выбрать, будет ли вызываться обычная или подвижная версия функции.

  • В каких ситуациях я хотел бы сказать компилятору, что он должен рассматривать переменную как значение? Это, скорее всего, произойдет в шаблонных или библиотечных функциях, где вы знаете, что можно получить промежуточный результат.

Кристофер Оезбек
источник
2
Большой +1 для примеров кода с семантикой в ​​комментариях. Другие топовые ответы определяют std :: move с помощью самого «move» - ничего толком не разъясняется! --- Я считаю, что стоит упомянуть, что отсутствие копии аргумента означает, что исходное значение не может быть надежно использовано.
ти
34

Сам std :: move на самом деле мало что делает. Я думал, что он вызвал перемещенный конструктор для объекта, но на самом деле он просто выполняет приведение типа (приведение переменной 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. Остальная часть использует семантику перемещения. Конструктор перемещения или оператор присваивания также должны быть реализованы для назначения результата A.

user929404
источник
3
Вы говорили о семантике перемещения. Вы должны добавить в свой ответ, как можно использовать std :: move, потому что вопрос об этом задается.
Кушик Шетти
2
@Koushik std :: move мало что делает, но используется для реализации семантики перемещения. Если вы не знаете о std :: move, вы, вероятно, тоже не знаете семантику перемещения
user929404
1
"не делает много" (да, просто static_cast для ссылки на rvalue). что на самом деле он делает, и у него это то, что спросил ОП. вам не нужно знать, как работает std :: move, но вы должны знать, что делает семантика перемещения. более того, «но используется для реализации семантики перемещения», наоборот. знаю семантику перемещения и вы поймете std :: move, иначе нет. Move просто помогает в движении и сам использует семантику движения. std :: move ничего не делает, кроме как преобразует свой аргумент в ссылку на rvalue, чего и требует семантика перемещения.
Кушик Шетти
10
«но E + F создает временный объект» - оператор +идет слева направо, а не справа налево. Следовательно B+C, будет первым!
Ajay
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также может быть полезно при сортировке массива элементов. Многие алгоритмы сортировки (такие как выборочная сортировка и пузырьковая сортировка) работают путем обмена парами элементов. Ранее нам приходилось прибегать к семантике копирования, чтобы выполнить обмен. Теперь мы можем использовать семантику перемещения, которая более эффективна.

Это также может быть полезно, если мы хотим переместить содержимое, управляемое одним умным указателем, в другое.

Цитируется:

Jayhello
источник
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;
}
Нил МакГилл
источник