Конструктор перемещения

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

Конструктор перемещения (move constructor) представляет альтернативу конструктору копирования в тех ситуациях, когда надо сделать копию объекта, но копирование данных нежелательно - вместо копирования данных они просто перемещаются из одной копии объекта в другую.

Рассмотрим на примере.

#include <iostream>
 
// класс сообщения
class Message
{
public:
    // обычный конструктор
    Message(const char* data, unsigned count)
    {
        size = count;
        text = new char[size];  // выделяем память
        for(unsigned i{}; i < size; i++)    // копируем данные
        {
            text[i] = data[i];
        }

        id = ++counter;
        std::cout << "Create Message " << id << std::endl;
    }
    // конструктор копирования
    Message(const Message& copy) : Message{copy.getText(), copy.size }  // обращаемся к стандартному конструктору
    {
        std::cout << "Copy  Message " << copy.id << " to " << id << std::endl;
    }
    
    // деструктор
    ~Message()
    { 
        std::cout << "Delete Message "  << id << std::endl;
        delete[] text;	// особождаем память
    }
    char* getText() const { return text; }
    unsigned getSize() const { return size; }
    unsigned getId() const {return id;}
private:
    char* text{};  // текст сообщения
    unsigned size{};    // размер сообщения
    unsigned id{};  // номер сообщения
    static inline unsigned counter{};   // статический счетчик для генерации номера объекта
};

// класс мессенджера, который отправляет сообщение
class Messenger
{
public:
    Messenger(Message mes): message{mes}
    { }
    void sendMessage() const
    {
        std::cout << "Send message " <<  message.getId() << ": " << message.getText() << std::endl;
    }
private:
    Message message;
};
int main()
{
    Messenger telegram{Message{"Hello Word", 11}};
    telegram.sendMessage();
}

Здесь определен класс условного сообщения Message. Текст сообщения хранится в символьном указателе text. Также, чтобы был виден весь процесс создания/копирования/удаления данных в классе сообщения определена статическая переменная counter, которая будет увеличиваться с созданием каждого нового объекта. И текущее значение счетчика будет присваиваться переменной id, которая представляет номер сообщения:

char* text{};  // текст сообщения
unsigned size{};    // размер сообщения
unsigned id{};  // номер сообщения
static inline unsigned counter{};

В конструкторе Message выделяем память для хранения текста сообщения, который передается через параметр - символьный массив, копируем данные в выделенную область памяти и устанавливаем номер сообщения. Для копирования данных в Message определен конструктор копирования.

Также определен класс Messenger, который принимает в конструкторе сообщение и сохраняет его в переменную message:

class Messenger
{
public:
	Messenger(Message mes): message{mes}
	{ }

С помощью функции sendMessage мессенджер условно отправляет сообщение.

В функции main создаем объект Messenger, передавая ему один объект сообщения, и затем вызываем функцию sendMessage

Messenger telegram{Message{"Hello Word", 11}};
telegram.sendMessage();

Обратите внимание, что в конструктор объекта Messenger здесь передается объект Message, который не привязан ни к какой переменной. Посмотрим на консольный вывод:

Create message 1
Create message 2
Copy  message 1 to 2
Delete message 1
Send message 2: Hello Word
Delete message 2

Здесь мы видим, что в процессе работы программы создается два объекта Message, причем вовлекается конструктор копирования. Посмотрим по этапно.

  1. Выполнение строки

    Messenger telegram{Message{"Hello Word", 11}};

    Приводит к выполнению конструктора Message, в котором строка "Hello Word" передается переменной text и устанавливает номер сообщения. Этот временный объект Message будет иметь номер 1. Соответственно на консоль выводится

    Create message 1
  2. Далее созданный объект Message передается в конструктор Messenger:

    Messenger(Message mes): message{mes}

    Обратите внимание на выражение message{mes}. Оно берет переданный в конструктор временный объект Message и с помощью конструктора копирования передает в переменную message его копию. Конструктор копирования Message в свою очередь обращается к стандартному конструктору:

    Message(const Message& copy) : Message{copy.getText(), copy.size }

    Создается объект Message номер 2. Стандартный конструктор выделяет память для строки. У нас получаются две копии, каждая из которых хранит указатели на разные участки памяти. То есть мы имеем две независимые копии, и на консоль будет выведено:

    Create message 2
    Copy  message 1 to 2
    Delete message 1
    

    Теперь объект Messenger хранит второе сообщение. Первое, временное сообщение удаляется.

  3. Далее вызывается функция sendMessage()

    telegram.sendMessage();

    Информация о хранимом в мессенджере сообщении выводится на консоль, и это сообщение по завершению работы функции main удаляется.

    Send message 2: Hello Word
    Delete message 2
    

С точки зрения создания копий, выделения/управления/освобождения памяти вроде проблем никаких нет. Но мы видим, что выделенная память для первого сообщения в итоге все равно никак не использовалась. То есть мы по сути зря использовали эту память. Не было бы лучше, если бы вместо выделения нового участка памяти для второго сообщения, мы могли бы просто передать ему память, которая уже выделена для первого сообщения? Первое же сообщение все равно удаляется. И для решения этой проблемы как раз используются конструкторы перемещения.

Конструктор перемещения принимает один параметр, который должен представлять ссылку rvalue на объект текущего типа:

MyClass(MyClass&& moved) // ссылка rvalue
{
	// код конструктора перемещения
}

Здесь параметр moved представляет перемещаемый объект.

Изменим выше приведенный код, применив конструктор перемещения в классе Message:

#include <iostream>
 
// класс сообщения
class Message
{
public:
    // обычный конструктор
    Message(const char* data, unsigned count)
    {
        size = count;
        text = new char[size];  // выделяем память
        for(unsigned i{}; i < size; i++)    // копируем данные
        {
            text[i] = data[i];
        }

        id = ++counter;
        std::cout << "Create Message " << id << std::endl;
    }
    // конструктор копирования
    Message(const Message& copy) : Message{copy.getText(), copy.size }  // обращаемся к стандартному конструктору
    {
        std::cout << "Copy  Message " << copy.id << " to " << id << std::endl;
    }
    Message(Message&& moved)
    {
        id = ++counter;
        std::cout << "Create Message " << id << std::endl;

        text = moved.text;  // перемещаем текст сообщения
        size = moved.size;  // перемещаем размер сообщения
        moved.text = nullptr;
        std::cout << "Move Message " << moved.id << " to " << id << std::endl;
    }
    // деструктор
    ~Message()
    { 
        std::cout << "Delete Message "  << id << std::endl;
        delete[] text;	// особождаем память
    }
    char* getText() const { return text; }
    unsigned getSize() const { return size; }
    unsigned getId() const {return id;}
private:
    char* text{};  // текст сообщения
    unsigned size{};    // размер сообщения
    unsigned id{};  // номер сообщения
    static inline unsigned counter{};   // статический счетчик для генерации номера объекта
};

// класс мессенджера, который отправляет сообщение
class Messenger
{
public:
    Messenger(Message mes): message{std::move(mes)}
    { }
    void sendMessage() const
    {
        std::cout << "Send message " <<  message.getId() << ": " << message.getText() << std::endl;
    }
private:
    Message message;
};
int main()
{
    Messenger telegram{Message{"Hello Word", 11}};
    telegram.sendMessage();
}

По сравнению с предыдущим кодом здесь сделаны два изменения. Прежде всего в класс Message добавлен конструктор перемещения:

Message(Message&& moved)
{
    id = ++counter;
    std::cout << "Create Message " << id << std::endl;

    text = moved.text;  // перемещаем текст сообщения
    size = moved.size;  // перемещаем размер сообщения
    moved.text = nullptr;
    std::cout << "Move Message " << moved.id << " to " << id << std::endl;
}

Здесь параметр moved представляет перемещаемый объект. Мы не вызваем стандартный конструктор, как в случае с конструктором копирования, потому что нам не надо выделять память. Вместо этого мы просто передаем в переменную text значение указателя (адрес блока выделенной памяти) из перемещаемого объекта moved:

text = moved.text

Таким образом мы избегаем ненужного дополнительного выделения памяти. И чтобы указатель text перемещаемого объекта moved перестал указывать на эту область памяти, и соответственно чтобы в деструкторе объекта moved не было ее освобождения, передаем указателю значение nullptr.

Другой важный момент - в конструкторе Messenger при копировании объекта используем встроенную функцию std::move(), которая имеется в стандартной библиотеке С++:

class Messenger
{
public:
	Messenger(Message mes): message{std::move(mes)}
	{ }

Функция std::move() преобразует переданное значение в ссылку rvalue. Несмотря на свое название, эта функция ничего не перемещает.

Выражение message{std::move(mes)} фактически приведет к вызову конструктора перемещения, в который будет передан параметр mes. А результат конструктора перемещения будет присвоен переменной message. Соответственно теперь мы получим другой консольный вывод:

Create message 1
Create Message 2
Move message 1 to 2
Delete message 1
Send message 2: Hello Word
Delete message 2

Если бы конструктор перемещения не был бы определен, то выражение message{std::move(mes)} вызывало бы конструктор копирования.

Таким образом, также создается два объекта, но теперь мы уходим от ненужного выделения памяти и копирования значения. Как правило, перещаются объекты, которые больше не нужны, как в примере выше.

Стоит отметить, что компилятор сам компилирует по умолчанию конструктор перемещения, который перемещает значения всех нестатических переменных. Однако если мы определяем деструктор или конструктор копирования или оператор присваивания с копированием или перемещением, то компилятор не генерирует стандартный конструктор перемещения.

Пример с векторами

Еще одним показательным примером применения конструктора перемещение может служить добавление объекта в вектор. Тип std::vector представляет динамический список и для добавления объекта определяет функцию push_back(). Эта функция имеет две версии:

void push_back(const Message &_Val)
void push_back(Message &&_Val)

Первая версия принимает константную ссылку и предназначена прежде всего для передачи lvalue. Вторая версия принимает rvalue.

Например, возьмем вышеопределенный класс Message и добавим один объект в вектор:

#include <iostream> 
#include <vector>

// класс сообщения
class Message
{
    // содержимое класса Message
}
int main()
{
	std::vector<Message> messages{};
	messages.push_back(Message{"Hello world", 12});
}

Здесь в вектор добавляется объект Message в виде rvalue. В своей внутренней реализации добавляемый объект будет сохраняться, и при сохранении будет вызываться конструктор перемещения, чтобы переместить данные из rvalue. Консольный вывод программы:

Create Message 1
Create Message 2
Move Message 1 to 2
Delete Message 1
Delete Message 2

Таким образом, опять же мы избегаем издержек при не нужном копировании данных, и вместо копирования перемещаем их.

Если же мы передадим в функцию push_back() значение lvalue, то будет вызываться другая версия функции, которая принимает константную ссылку, и в итоге будет вызываться конструктор копирования с созданием копии:

int main()
{
    Message mes{"Hello world", 12};
	std::vector<Message> messages{};
	messages.push_back(mes);
}

Консольный вывод программы:

Create message 1
Create message 2
Copy  message 1 to 2
Delete message 2
Delete message 1
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850