Идиома move-and-swap или перемещение с обменом применяется в операторах присвоения с перемещением. Она позволяет избежать дублирования кода деструктора и конструктора копирования. Суть данной идиомы состоит в следующей последовательности действий:
Для перемещаемого объекта создаем копию с помощью конструктора перемещения
Заменяем текущий объект измененной копией. Если же при изменении копии на каком-то этапе возникла ошибка, то текущий объект не заменяется.
Общая форма move-and-swap
выглядит следующим образом:
MyClass& MyClass::operator=(MyClass&& rhs) noexcept { MyClass moved(std::move(rhs)); // получаем перемещаемый объект swap(moved); // выполняем обмен значениями return *this; // возвращаем текущий объект }
Рассмотрим простейшую реализацию на следующем примере:
#include <iostream> class Message { public: Message(std::string data) : text{new std::string(data)} // выделяем память { id = ++counter; std::cout << "Create message " << id << std::endl; } // конструктор копирования Message(const Message& copy) : Message{copy.getText()} { std::cout << "Copy message " << copy.id << " to " << id << std::endl; } // конструктор перемещения Message(Message&& moved) noexcept : text{moved.text} { id = ++counter; std::cout << "Move message " << moved.id << " to " << id << std::endl; moved.text = nullptr; } ~Message() { std::cout << "Delete message " << id << std::endl; delete text; // освобождаем память } // присваивание с копированием Message& operator=(const Message& copy) { std::cout << "Copy assign message " << copy.id << " to " << id << std::endl; if (© != this) // избегаем самоприсваивания { *text = copy.getText(); } return *this; } // присваивание с перемещением Message& operator=(Message&& moved) noexcept { std::cout << "Move assign message " << moved.id << " to " << id << std::endl; Message temp{std::move(moved)}; // вызываем конструктор перемещения swap(temp); // обмен значениями return *this; // возвращаем текущий объект } // функция обмена void swap(Message& other) noexcept { std::swap(text, other.text); // обмениваем два указателя } std::string& getText() const { return *text; } unsigned getId() const {return id;} private: std::string* text; // текст сообщения unsigned id{}; // номер сообщения static inline unsigned counter{}; // статический счетчик для генерации номера объекта }; int main() { Message mes{""}; mes = Message{"hello"}; // присваивание с перемещением std::cout << "Message " << mes.getId() << ": " << mes.getText() << std::endl; }
Здесь определен класс условного сообщения Message. Текст сообщения хранится в динамической памяти и доступен через указатель text. Для большей наглядности используем обычные указатели, а не smart-указатели. Также, чтобы был виден весь процесс создания/копирования/удаления данных в классе сообщения определена статическая переменная counter, которая будет увеличиваться с созданием каждого нового объекта. И текущее значение счетчика будет присваиваться переменной id, которая представляет номер сообщения:
std::string* text; // текст сообщения unsigned id{}; // номер сообщения static inline unsigned counter{};
В конструкторе Message выделяем динамическую память для объекта std::string и устанавливаем номер сообщения:
Message(std::string data) : text{new std::string(data)} // выделяем память { id = ++counter; std::cout << "Create message " << id << std::endl; }
а в деструкторе освобождаем память
~Message() { std::cout << "Delete message " << id << std::endl; delete text; // освобождаем память }
Конструктор перемещения перемещает в текущий объект указатель на строку из перемещаемого объекта:
Message(Message&& moved) noexcept : text{moved.text} { id = ++counter; std::cout << "Move message " << moved.id << " to " << id << std::endl; moved.text = nullptr; }
В операторе присваивания с перемещением используем идиому move-and-swap:
Message& operator=(Message&& moved) noexcept { std::cout << "Move assign message " << moved.id << " to " << id << std::endl; Message temp{std::move(moved)}; // вызываем конструктор перемещения swap(temp); // обмен значениями return *this; // возвращаем текущий объект } // функция обмена void swap(Message& other) noexcept { std::swap(text, other.text); // обмениваем два указателя }
В операторе присваивания сначала создаем временный объект Message, в который перемещаем данные из перемещаемого объекта. Чтобы вызывался именно конструктор перемещения, применяем встроенную
функцию std::move()
, которая преобразует объект moved в rvalue
std::move(moved)
Затем с помощью функции swap обмениваем текущий и временный объект. В этой функции фактически вызываем встроенную функцию std::swap(), в которой обмениваем указатели двух объектов.
std::swap(text, other.text);
В функции main применяем оператор присваивания:
int main() { Message mes{""}; mes = Message{"hello"}; // присваивание с перемещением std::cout << "Message " << mes.getId() << ": " << mes.getText() << std::endl; }
Консольный вывод программы:
Create message 1 Create message 2 Move assign message 2 to 1 Move message 2 to 3 Delete message 3 Delete message 2 Message 1: hello Delete message 1
Здесь мы видим, что переменная mes будет представлять объект Message с номером 1. Выражение Message{"hello"}
будет создавать объект Message с номером 2.
При выполнении присвоения
mes = Message{"hello"};
начинает выполняться оператор присваивания с перемещением. В нем вызывается конструктор перемещения, который создает объект Message с номером 3 и перемещает в него данные из второго объекта
Message temp{std::move(moved)};
Далее обмениваем указатели объектов 1 и 3. При этом после использования для ненужных объектов 2 и 3 вызывается деструктор.