Урок №192. Функция std::move()

  Юрий  | 

  Обновл. 24 Авг 2020  | 

 18483

 ǀ   6 

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

Проблема

Очень часто объекты, с которыми вам придется работать, будут не r-values, а l-values. Например, рассмотрим следующий шаблон функции swap():

Принимая два объекта типа T (в данном случае std::string), функция swap() меняет местами их значения, делая при этом три копии.

Следовательно, результат выполнения программы:

x: Anton
y: Max
x: Max
y: Anton

А как мы уже знаем из предыдущего урока, копирование — это не очень эффективно. А поскольку мы выполняем копирование трижды, то это еще и сравнительно медленно. Мы можем этого избежать. Наша цель — поменять местами значения x и y. А для этого мы можем использовать перемещение, вместо копирования, сделав наш код более производительным!

Но как? Проблема состоит в том, что параметры x и y являются ссылками l-value, а не ссылками r-value, поэтому у нас нет способа вызвать конструктор перемещения или оператор присваивания перемещением вместо конструктора копирования и оператора присваивания копированием. По умолчанию у нас используется семантика копирования. Что делать?

Функция std::move()


Функция std::move() — это стандартная библиотечная функция, которая конвертирует передаваемый аргумент в r-value. Мы можем передать l-value в функцию std::move(), и std::move() вернет нам ссылку r-value. Для работы с std::move() нужно подключить заголовочный файл utility.

Вот вышеприведенная программа, но уже с функцией swap(), которая использует std::move() для преобразования l-values в r-values, чтобы мы имели возможность использовать семантику перемещения вместо семантики копирования:

Результат выполнения один и тот же:

x: Anton
y: Max
x: Max
y: Anton

Но эта версия программы гораздо эффективнее. При инициализации tmp, вместо создания копии x, мы используем std::move() для конвертации переменной x, которая является l-value, в r-value. А поскольку параметром становится r-value, то с помощью семантики перемещения x перемещается в tmp.

Затем, спустя несколько дополнительных перестановок, значение переменной x перемещается в переменную y, а значение y перемещается в переменную x.

Еще один пример

Мы также можем использовать std::move() для заполнения контейнерных классов (таких как std::vector) значениями l-values.

В следующей программе мы сначала добавляем элемент в вектор, используя семантику копирования, а затем добавляем элемент в вектор, используя семантику перемещения:

Результат выполнения программы:

Copying str
str: Bye
vector: Bye

Moving str
str:
vector: Bye Bye

В первом случае мы передаем l-value в push_back(), поэтому используется семантика копирования для добавления элемента в вектор. По этой причине переменная str остается с прежним значением.

Во втором случае мы передаем r-value (фактически l-value, которое конвертируется в r-value через std::move()) в push_back(), поэтому используется семантика перемещения для добавления элемента в вектор. Это более эффективно, так как элемент вектора может украсть значение переменной std::string, вместо его копирования. По этой же причине, str лишается своего значения.

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

Функции перемещения


Как мы уже говорили на предыдущем уроке, вы должны оставлять объекты, ресурсы которых вы перемещаете, в чётко определенном состоянии. В идеале это должно быть «нулевое состояние» (null/nullptr).

Теперь мы можем поговорить о том, почему при использовании функции std::move(), объект, ресурсы которого перемещаются, может не быть временным. Дело в том, что пользователь может захотеть повторно использовать этот же (теперь пустой) объект или протестировать его каким-либо образом.

В примере, приведенном выше, строка str остается пустой после выполнения перемещения (что всегда делает std::string после успешного перемещения). Таким образом, мы можем повторно её использовать, если, конечно, захотим или проигнорировать, если она нам больше не нужна.

Чем еще полезна функция std::move()?

Функция std::move() также может быть полезна при сортировке элементов массива. Многие алгоритмы сортировки (такие как «метод выбора» и «сортировка пузырьком») работают путем замены целых пар элементов. На предыдущих уроках нам приходилось использовать семантику копирования для выполнения таких замен. Теперь же мы можем использовать семантику перемещения, которая эффективнее.

Функция std::move() также может быть полезна при перемещении содержимого из одного умного указателя в другой.

Заключение


Функция std::move() может использоваться всякий раз, когда нужно обрабатывать l-value как r-value с целью использования семантики перемещения вместо семантики копирования.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (77 оценок, среднее: 5,00 из 5)
Загрузка...

Комментариев: 6

  1. Аватар Алексей:

    Добавил const в пример, и почему то код выполнился… Можете пояснить почем?

    1. Аватар Steindvart:

      Очень странно. Такое не должно компилироваться и у меня не компилируется — использую MVS.

  2. Аватар Анастасия:

    А поясните, пожалуйста, почему для приведённых примеров срабатывает оператор присваивания перемещением? Он встроен в std::vector и std::string? Мы сами его не создавали, а в прошлом уроке было правило, что если мы хотим, чтобы срабатывало именно перемещение, надо самим об этом позаботиться.

    1. Аватар Kotik:

      Правило «Если вам нужен конструктор перемещения и оператор присваивания перемещением, которые выполняют перемещение (а не копирование), то вам их нужно предоставить (написать) самостоятельно» относится только к тем объектам, что проектируем мы сами.

      1. Аватар Anonym:

        Есть правило Пяти:
        Если вам из 5 методов нужно что-то реализовать, то тогда уж надо реализовать остальные методы из пятерки:
        Деструктор
        Конструктор копирования
        Конструктор перемещения
        Присваивание копированием
        Присваивание перемещением

        Логика тут в том, что для простых типов пойдет лишь простой конструктор (которой инициализирует ваши поля, например).
        А Пятерку компилятор уже неявно соберет сам (Т.е. соберет эти специализированные методы по-умолчанию).

  3. Аватар kmish:

    Если изменить последний пример предыдущего урока следующим образом (напишу только измененные строчки):

    то есть, передать объект в функцию cloneArrayAndDouble() не по константной ссылке l-value, а по ссылке r-value. И замерить время выполнения, то при передаче по r-value время немного дольше. В среднем 0,005 против 0,0065 в пользу l-value на моей машине.
    Из чего можно сделать вывод, что если есть возможность выбрать, как мы передаем, то лучше передавать const DynamicArray<int> &arr, т.е. по константной ссылке l-value.

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *