M.4 – std::move

Добавлено 18 сентября 2021 в 13:26

Как только вы начнете использовать семантику перемещения более регулярно, вы начнете сталкиваться со случаями, когда вы хотите использовать семантику перемещения, но объекты, с которыми вам придется работать, являются l-значениями, а не r-значениями. Рассмотрим в качестве примера следующую функцию обмена:

#include <iostream>
#include <string>

template<class T>
void myswap(T& a, T& b)
{
  T tmp{ a };  // вызывает конструктор копирования
  a = b;       // вызывает присваивание копированием
  b = tmp;     // вызывает присваивание копированием
}

int main()
{
    std::string x{ "abc" };
    std::string y{ "de" };

    std::cout << "x: " << x << '\n';
    std::cout << "y: " << y << '\n';

    myswap(x, y);

    std::cout << "x: " << x << '\n';
    std::cout << "y: " << y << '\n';

    return 0;
}

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

x: abc
y: de
x: de
y: abc

Как мы показали в прошлом уроке, копирование может быть неэффективным. И эта версия функции обмена создает 3 копии. Это приводит к слишком большому количеству созданий и уничтожений строк, что происходит довольно медленно.

Однако здесь не нужно выполнять копирование. Всё, что мы действительно пытаемся сделать, это поменять местами значения a и b, что также можно сделать, используя вместо этого 3 перемещения! Поэтому, если мы переключимся с семантики копирования на семантику перемещения, мы сможем сделать наш код более производительным.

Но как? Проблема здесь в том, что параметры a и b являются lvalue-ссылками, а не rvalue-ссылками, поэтому у нас нет способа вызвать конструктор перемещения и оператор присваивания перемещением вместо конструктора копирования и присваивания копированием. По умолчанию мы получаем поведение конструктора копирования и присваивания копированием. Что же делать?

std::move

В C++11 std::move – это функция стандартной библиотеки, которая преобразует (используя static_cast) свой аргумент в rvalue-ссылку, чтобы можно было использовать семантику перемещения. Таким образом, мы можем использовать std::move, чтобы преобразовать l-значение в тип, который предпочтительнее перемещать, а не копировать. std::move определяется в заголовке utility.

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

#include <iostream>
#include <string>
#include <utility> // для std::move

template<class T>
void myswap(T& a, T& b)
{
  T tmp{ std::move(a) }; // вызывает конструктор перемещения
  a = std::move(b);      // вызывает присваивание перемещением
  b = std::move(tmp);    // вызывает присваивание перемещением
}

int main()
{
    std::string x{ "abc" };
    std::string y{ "de" };

    std::cout << "x: " << x << '\n';
    std::cout << "y: " << y << '\n';

    myswap(x, y);

    std::cout << "x: " << x << '\n';
    std::cout << "y: " << y << '\n';

    return 0;
}

Этот код печатает тот же результат, что и выше:

x: abc
y: de
x: de
y: abc

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

Еще через еще пару перемещений значение переменной x было перемещено в y, а значение y было перемещено в x.

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

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

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

#include <iostream>
#include <string>
#include <utility> // для std::move
#include <vector>

int main()
{
    std::vector<std::string> v;
    std::string str = "Knock";

    std::cout << "Copying str\n";
    // вызывает версию push_back для l-значений, которая копирует str в элемент массива
    v.push_back(str); 

    std::cout << "str: " << str << '\n';
    std::cout << "vector: " << v[0] << '\n';

    std::cout << "\nMoving str\n";
    // вызывает версию push_back для r-значений, которая перемещает str в элемент массива
    v.push_back(std::move(str)); 

    std::cout << "str: " << str << '\n';
    std::cout << "vector:" << v[0] << ' ' << v[1] << '\n';

    return 0;
}

Эта программа печатает:

Copying str
str: Knock
vector: Knock

Moving str
str:
vector: Knock Knock

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

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

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

Функции перемещения должны всегда оставлять ваши объекты в четко определенном состоянии

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

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

Где еще полезен std::move?

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

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

Заключение

std::move можно использовать везде, где мы хотим обрабатывать l-значение как r-значение, чтобы использовать семантику перемещения вместо семантики копирования.

Теги

C++ / Cppl-value / l-значениеLearnCppr-value / r-значениеstd::moveSTL / Standard Template Library / Стандартная библиотека шаблоновДля начинающихОбучениеПрограммированиеСемантика перемещенияУмные указатели

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.