Идиома move-and-swap или перемещение с обменом применяется в операторах присвоения с перемещением. Она позволяет избежать дублирования кода деструктора и конструктора копирования. Суть данной идиомы состоит в следующей последовательности действий:
Для перемещаемого объекта создаем копию с помощью конструктора перемещения
Заменяем текущий объект измененной копией. Если же при изменении копии на каком-то этапе возникла ошибка, то текущий объект не заменяется.
Общая форма move-and-swap выглядит следующим образом:
MyClass& MyClass::operator=(MyClass&& rhs) noexcept
{
MyClass moved(std::move(rhs)); // получаем перемещаемый объект
swap(moved); // выполняем обмен значениями
return *this; // возвращаем текущий объект
}
Рассмотрим простейшую реализацию на следующем примере:
#include <iostream>
#include <string>
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 вызывается деструктор.