На 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;
}
- Конструктор перемещения «заставляет» указатель объявленного объекта указывать на данные временного объекта, созданного благодаря ссылке Rvalue.
- Указатель временных объектов обнуляется, чем предотвращается проблема двойного удаления.
Сравнение конструктора копирования с конструктором перемещения
Пример:
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!
- Конструктор по умолчанию
Test()
вызывается при создании временного объекта внутриvec.push_back(Test())
, им выделяется динамическая память дляp
. - Конструктор перемещения не предоставляется, поэтому
Test(const Test& A)
вызывается для копирования временного объекта в векторvec
. - Внутри конструктора копирования выделяется новая динамическая память для
this->p
. - После вызова
push_back()
временный объект уничтожается, для чего вызывается деструктор, которым освобождается выделенная подp
динамическая память.
// С конструктором перемещения
Default constructor invoked
Move constructor invoked
Destructor invoked!
Destructor invoked!
- Конструктор по умолчанию
Test()
вызывается при создании временного объекта внутриvec.push_back(Test())
, им выделяется динамическая память дляp
. - Конструктор перемещения
Test(Test&& A)
вызывается для перемещения временного объекта в векторvec
. - Внутри конструктора перемещения владение динамической памятью, на которую указывается
A.p
, передается вthis->p
, а вA.p
во избежание двойного удаления задаетсяnullptr
. - После вызова
push_back()
временный объект уничтожается, для чего вызывается деструктор, которым освобождается выделенная подp
динамическая память.
Заключение
Конструктором копирования создается копия объекта в другом объекте, который занимает место в памяти.
А конструктором перемещения передается владение от одного объекта другому и в динамической памяти сохраняются данные. Дополнительного места в памяти семантикой перемещения не занимается.
Значит, перемещение быстрее копирования?
В целом, да. Хотя новые объекты создаются в стеке обоими конструкторами, в управлении динамической памятью обнаруживается различие. Если конструктором копирования обычно выделяется новая память в куче и выполняется глубокое копирование данных, то конструктором перемещения передается владение имеющейся динамической памятью — без лишних копирований и выделения памяти.
Однако для простых структур без косвенной адресации, указателей перемещения — это просто копии.
Читайте также:
- Создание RESTful API-интерфейсов на C++
- Оптимизация кода задачи на миллиард строк — ускоряем запуск в 87 раз
- [C++] часть 3: синхронизация потоков в ресторане
Читайте нас в Telegram, VK и Дзен
Перевод статьи Sylvain Tiset: Move to move constructor (C++)