Урок №192. std::move()

  Юрий  | 

    | 

  Обновл. 2 Июл 2019  | 

 4465

 ǀ   1 

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

Проблема

Очень часто объекты, с которыми вам придётся работать, будут не 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 (25 оценок, среднее: 5,00 из 5)
Загрузка...

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

  1. Аватар kmish:

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

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

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

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