Я пытаюсь понять семантику перемещения С++, конструктор перемещения, оператор присваивания перемещения, std::move(). Рассмотрим следующий пример:
#include <iostream>
void swapWithMove(int& a, int& b) {
int temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
int main() {
int x = 5;
int y = 10;
std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;
swapWithMove(x, y);
std::cout << "After swap: x = " << x << ", y = " << y << std::endl;
return 0;
}
В этом примере два целочисленных значения заменяются с помощью std::move(). Я знаю, что пример больше для образовательных целей и не является типичным случаем использования семантики перемещения. Но все же хотелось бы правильно понять, что происходит в памяти с x, y, temp, a и b при выполнении функции swapWithMove.
Заранее большое спасибо!
Или, может быть, это? stackoverflow.com/questions/27888873/copy-vs-stdmove-for-ints
Буквально ничего не происходит в памяти, когда используется семантика перемещения. Это всего лишь средство сделать выбор разрешения перегрузки перегрузкой, использующей ссылку rvalue, если таковая имеется.
«не является типичным случаем использования семантики перемещения»: как следствие того, что сказано в комментарии выше, ваше использование в вашем примере на 100% точно идентично тому же коду без вызовов std::move
.
Для целочисленных типов копирование и перемещение абсолютно одинаковы. std::move
здесь бессмысленно и запутанно. Если вы знаете, что происходит в памяти для функции подкачки, которая копирует целочисленные аргументы, вы знаете, что происходит в памяти при перемещении целочисленных аргументов.
Прочтите это: stackoverflow.com/a/21358433/576911. Ответ, по-видимому, касается только части именования. Но если вы прочитаете конец этого ответа, он даст очень хороший ответ на ваш вопрос: что именно делает перемещение?
При данных обстоятельствах (замена int
s) семантика перемещения не будет иметь никакого значения, с любой реализацией, о которой я знаю (и исключение было бы несколько удивительным).
Семантика перемещения в значительной степени вступает в игру, когда мы имеем дело с чем-то вроде vector
, который (в основном) хранит указатель на содержащиеся в нем данные. Например, давайте рассмотрим несколько упрощенную реализацию std::vector
:
template <class T>
class vector {
T *data;
std::size_t size_allocated;
std::size_t size_in_use;
// ...
Здесь важно отметить, что сама структура vector
не содержит реальных данных. Он просто содержит указатель на данные, которые выделяются везде, где их получает объект Allocator
(но обычно с использованием стандартного распределителя, который получает память из свободного хранилища.
В любом случае, давайте рассмотрим задание для нашего vector
:
vector &operator=(vector const &other) {
if (size_allocated < other.size_allocated) {
// reallocate our storage so we have enough room
}
size_in_use = other.size_in_use;
for (std::size_t i=0; i<other.size_in_use; i++)
data[i] = other.data[i];
return *this;
}
Это сильно упростило, но вы поняли общую идею — пройтись по всем элементам и скопировать каждый из старого вектора в новый.
Но если правый — это rvalue
, это означает, что нам не нужно сохранять его содержимое. Так что мы можем просто «украсть» то, что в нем содержится:
vector &operator=(vector &&other) {
delete [] data; // simplified--really uses allocator object
data = other.data;
size_in_use = other.size_in_use;
size_allocated = other.size_allocted;
other.data = nullptr;
other.size_allocated = 0;
other.size_in_use = 0;
return *this;
}
Таким образом, вместо того, чтобы копировать каждый элемент по отдельности, мы просто берем указатель из источника и превращаем источник в пустой вектор. Очень быстро, независимо от его размера. Однако есть немного хитрый способ немного упростить это:
vector &operator=(vector &&other) {
swap(data, other.data);
swap(size_in_use, other.size_in_use);
swap(size_allocated, other.size_allocated);
return *this;
}
Просто поменяйте наше содержимое на чужое. Затем он будет уничтожен, а то, что раньше было нашим содержимым, будет утилизировано.
В любом случае: перемещение int
обычно ничем не отличается от обычного задания. То же самое с большинством других скалярных типов (char
, short
, long
, double
, float
, указатели и т. д.). Различия возникают для структурированных типов, особенно тех, которые в основном содержат указатель на реальные данные, которые они хранят.
"(и исключение было бы несколько неожиданным)": И должно быть несоответствующим. Допускается только одно возможное поведение, и оно совпадает с поведением без std::move
.
@ user17732522: К сожалению, я работаю достаточно долго, чтобы найти несоответствие лишь несколько удивительным. Но в этом случае, да, это было бы довольно вопиюще несоответствующим, а не чем-то непонятным, что вы, вероятно, никогда не заметите.
Возможно, стоит упомянуть, что это не обязательно имеет значение для типов структур; если конструктор перемещения/оператор присваивания недоступен, но доступна версия копирования, ссылка rvalue будет неявно преобразована в ссылку lvalue на const, и будет использоваться версия копирования. Например. класс может быть реализован как struct Foo{ Foo(); Foo(Foo const&); Foo& operator=(Foo const&); ...stuff other than constructors/assignment operators... };
@JerryCoffin Ваш пример очень поучителен и понятен, большое спасибо за это! Таким образом, перемещение чрезвычайно полезно, когда внутреннее представление класса использует указатели. И тогда мы можем просто украсть указатель временного объекта вместо того, чтобы иметь дело с глубоким копированием. Также я не знал, что в моем примере (функция swapWithMove) нет никакой разницы, применяю ли я std::move() в реализации или нет. Я подумал, что реализация с std::move() как-то более эффективна, чем если бы я реализовал функцию подкачки без нее.
@JerryCoffin Спасибо, что указали, что с точки зрения эффективности нет никакой разницы.
Отвечает ли это на ваш вопрос? Что такое std::move(), когда его следует использовать и действительно ли он что-то перемещает?