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

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

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

9 ответов

Решение

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

  1. В C++11, помимо конструкторов копирования, объекты могут иметь конструкторы перемещения.
    (И в дополнение к операторам копирования у них есть операторы перемещения).
  2. Конструктор перемещения используется вместо конструктора копирования, если объект имеет тип "rvalue-reference" (Type &&).
  3. std::move() это приведение, которое создает rvalue-ссылку на объект, чтобы позволить ему двигаться от него.

Это новый C++ способ избежать копирования. Например, используя конструктор перемещения, std::vector может просто скопировать свой внутренний указатель на данные в новый объект, оставив перемещенный объект в неправильном состоянии, избегая копирования всех данных. Это будет C++- допустимо.

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

1. "Что это?"

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

2. "Что это делает?"

Первое, что нужно отметить, это то, что std::move() на самом деле ничего не двигает.

Если вы когда-либо смотрели анимационный сериал " Блич" - это эквивалентно смягчению Рейши Куинси Зеле Шнайдер.

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

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

другими словами, когда вы используете std::move(x)вы позволяете компилятору каннибализировать x, Таким образом, если x имеет, скажем, свой собственный буфер в памяти - после std::move()у компилятора может быть другой объект, которому он принадлежит.

3. "Когда его следует использовать?"

Другой способ задать этот вопрос: "Для чего я мог бы уничтожить ресурсы существующего объекта?" хорошо, если вы пишете код приложения, вы, вероятно, не будете много возиться с временными объектами, созданными компилятором. Так что в основном вы должны делать это в таких местах, как конструкторы, операторные методы, функции, подобные алгоритму STL и т. Д., Где объекты создаются и уничтожаются автоматически. Конечно, это просто правило.

Типичное использование - это "перемещение" ресурсов с одного объекта на другой вместо копирования. @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 указателя на буферы векторов. Конечно, класс T должен знать, как сделать движение; у вас должен быть оператор присваивания перемещения и конструктор перемещения для класса T, чтобы это работало.

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

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

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

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

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

Q: что такое std::move?

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

Simplisticly 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&) или ссылка на значение (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 (временным выражением).

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

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

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

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

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

Сам 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.

What is it? а также What does it do? было объяснено выше.

Я приведу пример when it should be used.

Например, у нас есть класс с большим количеством ресурсов, таких как большой массив.

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 также может быть полезен при сортировке массива элементов. Многие алгоритмы сортировки (такие как выборочная сортировка и пузырьковая сортировка) работают путем обмена парами элементов. Раньше нам приходилось прибегать к семантике копирования, чтобы выполнить обмен. Теперь мы можем использовать семантику перемещения, которая более эффективна.

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

Цитируется:

https://www.learncpp.com/cpp-tutorial/15-4-stdmove/

std::move сам ничего не делает, а static_cast. Согласно cppreference.com

Это в точности эквивалентно static_cast ссылочному типу rvalue.

Таким образом, это зависит от типа переменной, которую вы назначаете после move, если тип имеет constructors или assign operatorsкоторый принимает параметр rvalue, он может или не может украсть содержимое исходной переменной, поэтому он может оставить исходную переменную вunspecified state:

Если не указано иное, все объекты стандартной библиотеки, которые были перемещены, не переводятся в допустимое, но неуказанное состояние.

Потому что нет особенного move constructor или move assign operator для встроенных литеральных типов, таких как целые числа и необработанные указатели, поэтому он будет простой копией для этих типов.

Вот полный пример использования 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;
}

std::move просто приводит переменную к ссылке rvalue. Эта ссылка на rvalue обозначается символом &&. Допустим, у вас есть класс Foo, и вы создаете такой объект.

      Foo foo = Foo();

Если потом написать


это то же самое, что если бы я написал


std::move заменяет это приведение на ссылку rvalue. Причина, по которой вы хотели бы написать любую из двух предыдущих строк кода, заключается в том, что если вы напишете

      Foo foo2 = foo;

Будет вызван конструктор копирования. Допустим, у экземпляров Foo есть указатель на некоторые данные в куче, которой они владеют. В деструкторе Foo эти данные в куче удаляются. Если вы хотите провести различие между копированием данных из кучи и владением этими данными, вы можете написать конструктор, который принимает const Foo&, и этот конструктор может выполнять глубокое копирование. Затем вы можете написать конструктор, который принимает ссылку rvalue (Foo&&), и этот конструктор может просто переназначить указатели. Этот конструктор, который принимает Foo&&, будет вызываться при написании

      Foo foo2 = std::move(foo);

и когда ты пишешь

      Foo foo2 = (Foo&&) foo;
Другие вопросы по тегам