Семантика перемещения
Может оказаться что операция копирования (конструктор копирования или оператор присваивания) достаточно дорогая для определенного класса (например для String: посимвольное копирование) и можно обойтись простым перемещением.
Если конструктор копирования это “скопируй данные у этого объекта” (а сам объект не изменяй, поскольку он передается по константной ссылке), то перемещающий конструктор это “отбери данные у этого объекта”. Это и называется перемещение:
struct String
{
String(String && other) : data(other.data), size(other.size) // отбираем данные
{
// оставляем "ограбленный" объект в согласованном состоянии
other.size = 0;
other.data = nullptr;
}
};
Мы получаем объект по rvalue-ссылке потому, что если бы передали в конструктор lvalue-ссылку то вызвался бы конструктор копирования.
ВНИМАНИЕ! “Ограбленный” объект нужно всегда оставлять в согласованном состоянии, мы не можем его просто так бросить, как минимум, потому что когда-то у него вызовется деструктор, который почистит отобранную нами область памяти. Этот объект можно “занулить”, но лучше всего воспользоваться std::swap (о нём ниже).
Аналогично перемещающий оператор присваивания:
struct String
{
Struct & operator=(String && other)
{
if (this != &other) // "обворовывать" самого себя - глупо
{
// освобождаем текущую область памяти
delete [] data;
// отбираем данные
data = other.data;
size = other.size;
// оставляем в согласованном состоянии
other.data = nullptr;
other.size = 0;
}
return *this;
}
};
В перемещающем конструкторе копирования необходимо так же очищать текущую занятую область памяти, в перемещающем конструкторе делать это было ненужно, поскольку конструктор вызывается когда объект еще не создан и не владеет никакой областью памяти!
std::swap()
Очень удобно использовать std::swap()
из заголовка #include <utility>
для реализации перемещения (просто меняет значения двух объектов между собой):
#include <utility>
struct String
{
// реализуем метод swap
void swap(String & other)
{
std::swap(data, other.data);
std::swap(size, other.size);
}
// перемещающий конструктор
String(String && other)
{
// что бы после обмена было согласованное состояние
data = nullptr;
size = 0;
swap(other);
}
// перемещающий оператор копирования
String & operator=(String && other)
{
if (this != &other)
{
swap(other);
}
return *this;
}
};
Однако теперь нужно следить в перемещающем конструкторе за согласованым состоянием после обмена, поскольку объект, который “обворовывает” еще не создан!
Когда используются перемещающие методы
Если передавать в них:
- Объект при помощи
std::move()
- Временный объект
Примеры:
String a(String("Hello")); // временный объект -> rvalue -> перемещающий конструктор
String b(a); // конструктор копирования
String c(std::move()); // std::move() -> rvalue -> перемещающий конструктор
a = b; // оператор присваивания
b = std::move(c); // std::move() -> rvalue -> перемещающий оператор присваивания
c = String("world"); // временный объект -> перемещающий оператор присваивания
Эти правила справедливы и для любых других методов и функций в которых происходит перегрузка между rvalue-ссылками и lvalue-ссылками!
Особые методы
С появлением перемещающих методов, расширился список особых методов, которые компилятор умеет генерировать самостоятельно:
- Конструктор по умолчанию
- Конструктор копирования
- Оператор присваивания
- Деструктор
- Перемещающий конструктор
- Перемещающий оператор присваивания
Однако правила генерации новых методов отличаются от старых: перемещающие методы генерируются только, если в классе отсутствуют другие особые методы либо с ключевым словом default.
Правила генерации других особых методов признаны устаревшими и могут быть изменены в будущем.