M.4 – std::move
Как только вы начнете использовать семантику перемещения более регулярно, вы начнете сталкиваться со случаями, когда вы хотите использовать семантику перемещения, но объекты, с которыми вам придется работать, являются 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-значение, чтобы использовать семантику перемещения вместо семантики копирования.