Идиома Move-and-Swap / Перемещение с обменом

Последнее обновление: 24.06.2023

Идиома move-and-swap или перемещение с обменом применяется в операторах присвоения с перемещением. Она позволяет избежать дублирования кода деструктора и конструктора копирования. Суть данной идиомы состоит в следующей последовательности действий:

  1. Для перемещаемого объекта создаем копию с помощью конструктора перемещения

  2. Заменяем текущий объект измененной копией. Если же при изменении копии на каком-то этапе возникла ошибка, то текущий объект не заменяется.

Общая форма 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 вызывается деструктор.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850