Что такое move constructor и оператор move assignment?
Move constructor (T&& other) и move assignment (operator=(T&&)) «крадут» ресурсы временного объекта, обнуляя источник. Обязательно noexcept — иначе std::vector при reallocate не использует move. После std::move объект в valid-but-unspecified состоянии.
Move семантика в C++: конструктор перемещения и move assignment
Move семантика, появившаяся в C++11, позволяет «украсть» ресурсы временного объекта вместо их дорогостоящего копирования. Это критично для классов, владеющих heap-памятью, файловыми дескрипторами или сетевыми соединениями.
Проблема до C++11
До C++11 передача объектов в функции и возврат из них всегда вызывали копирующий конструктор — даже если источник являлся временным объектом и сразу уничтожался.
Rvalue references
T&& — rvalue-ссылка, привязывается только к rvalue (временным объектам или результату std::move). Это позволяет перегружать конструктор и оператор присваивания специально для «переезжающих» объектов.
Реализация move constructor и move assignment
#include <cstddef>
#include <utility>
#include <iostream>
#include <algorithm>
class Buffer {
public:
// Конструктор
explicit Buffer(std::size_t size)
: data_(new int[size]), size_(size) {
std::cout << "construct size=" << size << "\n";
}
// Деструктор
~Buffer() {
delete[] data_;
std::cout << "destroy size=" << size_ << "\n";
}
// Копирующий конструктор
Buffer(const Buffer& other)
: data_(new int[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "copy size=" << size_ << "\n";
}
// Move constructor — «кражa» ресурсов
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // обнулить источник!
other.size_ = 0;
std::cout << "move construct\n";
}
// Copy assignment
Buffer& operator=(const Buffer& other) {
if (this != &other) {
Buffer tmp(other); // copy-and-swap
swap(tmp);
}
return *this;
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_; // освободить свои ресурсы
data_ = other.data_; // украсть
size_ = other.size_;
other.data_ = nullptr; // обнулить источник
other.size_ = 0;
}
std::cout << "move assign\n";
return *this;
}
void swap(Buffer& other) noexcept {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
}
std::size_t size() const noexcept { return size_; }
private:
int* data_;
std::size_t size_;
};
int main() {
Buffer a(100);
Buffer b = std::move(a); // move constructor
std::cout << "a.size=" << a.size() << "\n"; // 0 — a опустошён
std::cout << "b.size=" << b.size() << "\n"; // 100
Buffer c(50);
c = std::move(b); // move assignment
std::cout << "b.size=" << b.size() << "\n"; // 0
std::cout << "c.size=" << c.size() << "\n"; // 100
}
Rule of Five
Если класс явно определяет хотя бы один из: деструктор, copy constructor, copy assignment, move constructor, move assignment — нужно объявить все пять. Иначе компилятор может не сгенерировать нужные специальные функции автоматически.
noexcept обязателен для move
STL-контейнеры (vector, deque) используют move вместо copy только при reallocate, если move constructor помечен noexcept. Без noexcept вектор будет копировать элементы при росте — нивелируя весь выигрыш.
std::move не перемещает
std::move(x) — это просто cast к rvalue-ссылке. Само перемещение выполняет move constructor/assignment при вызове. После std::move объект находится в «valid but unspecified state» — можно только уничтожить или переназначить.
Когда компилятор генерирует move автоматически
Компилятор генерирует move constructor и move assignment, если класс не объявляет явно ни копирующих операций, ни деструктора. Объявление любого из них подавляет автогенерацию move.
Подводные камни
- Забыть обнулить указатель в источнике: деструктор источника вызовет
delete[]второй раз — double free. - Move constructor без
noexcept:std::vectorпри resize будет копировать, а не перемещать — потеря производительности. - Использование перемещённого объекта: объект в «moved-from» состоянии имеет неопределённое значение — читать его опасно.
- Self-move (
a = std::move(a)): без проверкиthis != &otherможно уничтожить свои данные раньше их кражи. - Move не всегда быстрее copy: для мелких trivially-copyable типов (int, double) компилятор оптимизирует оба пути одинаково.
- Объявление деструктора блокирует автогенерацию move — нужно явно писать
= default. - Передача по значению + std::move внутри (sink idiom) может дать лишнюю копию при передаче lvalue — иногда лучше две перегрузки.
Common mistakes
- Объяснять move constructor и move assignment только по синтаксису, без жизненного цикла и стоимости.
- Игнорировать ошибки, null/empty состояния, порядок инициализации или режим сборки.
- Давать пример, который работает в демо, но ломается при изменении владельца ресурса.
- Показывать сырой указатель без объяснения владельца и момента освобождения.
What the interviewer is testing
- Кандидат формулирует точную модель для move constructor и move assignment, а не только определение.
- Пример компилируем, безопасен по lifetime и соответствует версии технологии.
- Названы trade-off, ограничения и диагностируемые симптомы ошибки.