На C++11 появился новый функционал, в том числе конструкторы перемещения. Разберем их отличия от конструктора копирования и варианты применения.

Предварительные условия

Lvalue и Rvalue

Lvalue относится к ячейке памяти, которой идентифицируется объект.

  • Для задания псевдонима имеющемуся объекту.
  • Для реализации семантики передачи по ссылке.

Rvalue относится к значению данных, которое хранится по некоторому адресу в памяти.

  • Продлевается время жизни временного объекта, которому значения Rvalue присваиваются.
  • rvalue модифицируется неконстантными ссылками rvalue.
  • Работа с конструктором перемещения и присваиванием перемещения.
  • Нельзя привязать неконстантную ссылку lvalue, например int&, к rvalue, например int.
  • Нельзя привязать ссылки rvalue, например int&&, к lvalue, например int.
int a = 10;

// Объявление «lvalue»-ссылки
int& lref = a;

// Объявление «rvalue»-ссылки
int&& rref = 20;

Rvalues  —  это фактические значения с истекающим временем жизни, которые нельзя использовать напрямую. Доступ к ним получается rvalue-ссылками.

Поверхностное копирование и глубокое копирование

При поверхностном копировании объекта копируются все значения полей-элементов.

  • Стандартный метод копирования конструктора копирования.
  • Эти два объекта не являются независимыми.
Поверхностное копирование

При глубоком копировании копируются все поля и делаются копии динамически выделяемой памяти, на которую указывается полями.

  • Должен реализовываться определяемый пользователем конструктор.
Глубокое копирование

Семантика копирования и перемещения

Семантикой копирования называется перенос реальных данных объекта в другой объект путем создания копии в памяти.

Семантика перемещения нацелена на передачу владения ресурсами от одного объекта к другому.

Связь с конструкторами копирования и перемещения

Конструкторами копирования C++ используются ссылки Lvalue и семантика копирования для копирования данных одного объекта в другой объект того же класса.

Конструкторами перемещения C++ используются ссылки Lvalue и семантика перемещения, которой при помощи ссылок rvalue указывается на имеющийся элемент в памяти.

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

Конструктор копирования  —  это функция-член, которой инициализируется объект с помощью другого, уже имеющегося объекта того же класса.

  • Конструктором копирования в качестве аргумента принимается ссылка Lvalue на объект того же класса.
  • Конструктором копирования по умолчанию применяется поверхностное копирование, в то время как конструктором, определяемым пользователем,  —  глубокое.
  • Конструктором копирования используются ссылка Lvalue и семантика копирования.
Test(Test &t)
{
id=t.id;
}

Конструктор перемещения  —  это функция-член для эффективного переноса ресурсов, например динамически выделяемой памяти, из одного объекта в другой. Второе его название  —  украденный конструктор.

  • Конструктором перемещения в качестве аргумента принимается ссылка Rvalue на объект того же класса.
  • Конструктором перемещения применяется поверхностное копирование  —  перед присвоением пустой ссылки источнику.
  • Конструктором перемещения используются ссылка Rvalue и семантика перемещения.
// время жизни «другого» скоро истекает, это ссылка Rvalue
Vector::Vector(Vector&& other)
// поверхностное копирование
: storage_(other.storage_)
, size_(other.size_)
{
// источнику присваивается пустая ссылка
other.storage_ = nullptr;
other.size_ = 0;
}
  1. Конструктор перемещения «заставляет» указатель объявленного объекта указывать на данные временного объекта, созданного благодаря ссылке Rvalue.
  2. Указатель временных объектов обнуляется, чем предотвращается проблема двойного удаления.
Процесс конструктора перемещения согласно Kholdstare

Сравнение конструктора копирования с конструктором перемещения

Пример:

class Test{
private:
int *p;

public:
Test()
{
cout << "Default constructor invoked\n";
p = new int;
}

Test(const Test& A)
{
this->p = new int;
cout << "Copy constructor invoked\n";
}

Test(Test&& A)
{
cout << "Move constructor invoked\n";
this->p = A.p;
A.p = nullptr;
}

~Test()
{
cout <<"Destructor invoked!\n";
delete p;
}

};

int main()
{
vector <Test> vec;
vec.push_back(Test());
return 0;
}

Вот результат вывода без конструктора перемещения и с ним же:

// Без конструктора перемещения
Default constructor invoked
Copy constructor invoked
Destructor invoked!
Destructor invoked!
  1. Конструктор по умолчанию Test() вызывается при создании временного объекта внутри vec.push_back(Test()), им выделяется динамическая память для p.
  2. Конструктор перемещения не предоставляется, поэтому Test(const Test& A) вызывается для копирования временного объекта в вектор vec.
  3. Внутри конструктора копирования выделяется новая динамическая память для this->p.
  4. После вызова push_back() временный объект уничтожается, для чего вызывается деструктор, которым освобождается выделенная под p динамическая память.
// С конструктором перемещения
Default constructor invoked
Move constructor invoked
Destructor invoked!
Destructor invoked!
  1. Конструктор по умолчанию Test() вызывается при создании временного объекта внутри vec.push_back(Test()), им выделяется динамическая память для p.
  2. Конструктор перемещения Test(Test&& A) вызывается для перемещения временного объекта в вектор vec.
  3. Внутри конструктора перемещения владение динамической памятью, на которую указывается A.p, передается в this->p, а в A.p во избежание двойного удаления задается nullptr.
  4. После вызова push_back() временный объект уничтожается, для чего вызывается деструктор, которым освобождается выделенная под p динамическая память.

Заключение

Конструктором копирования создается копия объекта в другом объекте, который занимает место в памяти.

А конструктором перемещения передается владение от одного объекта другому и в динамической памяти сохраняются данные. Дополнительного места в памяти семантикой перемещения не занимается.

Значит, перемещение быстрее копирования?

В целом, да. Хотя новые объекты создаются в стеке обоими конструкторами, в управлении динамической памятью обнаруживается различие. Если конструктором копирования обычно выделяется новая память в куче и выполняется глубокое копирование данных, то конструктором перемещения передается владение имеющейся динамической памятью  —  без лишних копирований и выделения памяти.

Однако для простых структур без косвенной адресации, указателей перемещения  —  это просто копии.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Sylvain Tiset: Move to move constructor (C++)

Предыдущая статьяСоздание локального озера данных с нуля
Следующая статьяСети Колмогорова-Арнольда (KAN) могут навсегда изменить мир ИИ