move semantics
Явная передача владения ресурсом объекта вместо его копирования. std::move ничего не перемещает сам — это приведение к rvalue-ссылке, которое включает выбор move-конструктора/move-присваивания при перегрузке.Explicit transfer of an object's resource ownership instead of copying it. std::move doesn't move anything by itself — it's a cast to an rvalue reference that triggers overload resolution to pick the move constructor/move assignment operator.
std::move(x) — функция из <utility>, которая ничего не двигает физически, а лишь приводит выражение x к типу rvalue-ссылки (static_cast<T&&>(x)), сигнализируя компилятору «этим объектом больше не пользуются, можно забрать его ресурсы». Move-конструктор и move-присваивание — специальные функции-члены, принимающие T&&, которые реализуют это «забирание» — обычно перекладыванием указателя вместо глубокого копирования данных.std::move(x) is a function from <utility> that doesn't physically move anything — it merely casts the expression x to an rvalue reference type (static_cast<T&&>(x)), signaling to the compiler "this object is no longer needed, its resources can be taken." The move constructor and move assignment operator are special member functions taking T&& that implement this "taking" — usually by transferring a pointer instead of deep-copying data.
std::vector<std::string> a = {"x", "y"};
std::vector<std::string> b = a; // глубокая копия всех строк// a deep copy of every string
std::vector<std::string> a = {"x", "y"};
std::vector<std::string> b = std::move(a);
// b забрал внутренний буфер a; a теперь в валидном,// b took over a's internal buffer; a is now in a valid,
// но неопределённом (обычно пустом) состоянии// but unspecified (usually empty) state
Move semantics устраняет системную неэффективность, встроенную в язык с самого начала:Move semantics eliminates a systemic inefficiency that had been built into the language from the start:
До C++11 передача владения ресурсом (буфером, файловым дескриптором) всегда шла через глубокую копию — даже когда источник был временным объектом, который всё равно исчезнет сразу после операции. Move устраняет эту избыточную работу без изменения внешнего интерфейса типа.Before C++11, transferring resource ownership (a buffer, a file handle) always went through a deep copy — even when the source was a temporary that was about to disappear right after the operation anyway. Move eliminates that redundant work without changing the type's external interface.
std::vector при росте буфера до C++11 копировал каждый элемент в новый буфер — с move-конструктором элементы перемещаются, что для типов вроде std::string или std::vector на порядок дешевле.Before C++11, std::vector copied every element into the new buffer when it grew — with a move constructor, elements are moved instead, which for types like std::string or std::vector is an order of magnitude cheaper.
Без move semantics невозможно было бы выразить тип с уникальным владением ресурсом (файл, поток, память) без разрешения его копирования — std::unique_ptr, std::thread и std::future существуют благодаря именно этому механизму.Without move semantics it would be impossible to express a type with unique ownership of a resource (a file, a thread, memory) without also allowing it to be copied — std::unique_ptr, std::thread, and std::future exist precisely because of this mechanism.
Move-конструктор — конструктор с сигнатурой T(T&& other), move-присваивание — T& operator=(T&& other). Оба обычно реализуются как «перекладывание» внутренних указателей/дескрипторов от other к *this с обнулением полей other, чтобы деструктор other не освободил ресурс, который теперь принадлежит новому объекту.The move constructor is a constructor with the signature T(T&& other); move assignment is T& operator=(T&& other). Both are typically implemented as "transferring" internal pointers/handles from other to *this while zeroing out other's fields, so that other's destructor doesn't free a resource that now belongs to the new object.
class Buffer {
size_t size_ = 0;
int* data_ = nullptr;
public:
Buffer(size_t n) : size_(n), data_(new int[n]) {}
// move-конструктор — забирает указатель, обнуляет источник// move constructor — takes the pointer, zeroes out the source
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr; // деструктор other теперь безопасен// other's destructor is now safe
}
// move-присваивание// move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_; size_ = other.size_;
other.data_ = nullptr; other.size_ = 0;
}
return *this;
}
~Buffer() { delete[] data_; } // безопасно для обнулённого data_// safe for a nulled-out data_
};
Buffer make() { return Buffer(1000); } // возврат — move (или RVO — без move вовсе)// the return is a move (or RVO — no move at all)
Buffer b = make();
Компилятор автоматически генерирует move-конструктор/move-присваивание (как и copy-версии), если класс не объявляет пользовательский деструктор, copy-конструктор или copy-присваивание — правило иногда называют «rule of five» (расширение «rule of three» из до-C++11 эпохи).The compiler auto-generates the move constructor/move assignment (as well as the copy versions) if the class doesn't declare a user-provided destructor, copy constructor, or copy assignment operator — this rule is sometimes called the "rule of five" (an extension of the pre-C++11 "rule of three").
Вариант вопроса на интервью — не «объясни move», а «допиши класс до rule of five»: дают RAII-обёртку с одним ресурсом и просят дописать все пять специальных функций-членов, обычно начиная с уже готового деструктора.A common interview variant isn't "explain move" but "complete this class to satisfy the rule of five": you're given a RAII wrapper around a single resource and asked to write all five special member functions, usually starting from an already-provided destructor.
rule_of_five.hpp
class Buffer {
size_t size_ = 0;
int* data_ = nullptr;
public:
explicit Buffer(size_t n) : size_(n), data_(new int[n]) {}
// 1. деструктор// 1. destructor
~Buffer() { delete[] data_; }
// 2. copy-конструктор — глубокая копия// 2. copy constructor — deep copy
Buffer(const Buffer& other)
: size_(other.size_), data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
}
// 3. copy-присваивание — идиома copy-and-swap, exception-safe// 3. copy assignment — copy-and-swap idiom, exception-safe
Buffer& operator=(const Buffer& other) {
Buffer tmp(other); // может кинуть исключение — *this ещё не тронут// may throw — *this hasn't been touched yet
std::swap(size_, tmp.size_);
std::swap(data_, tmp.data_);
return *this; // старые данные tmp удалит его же деструктор// tmp's own destructor will free the old data
}
// 4. move-конструктор — забрать указатель, обнулить источник// 4. move constructor — take the pointer, zero out the source
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
}
// 5. move-присваивание// 5. move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_; size_ = other.size_;
other.data_ = nullptr; other.size_ = 0;
}
return *this;
}
};
Что обычно проверяют дополнительно: почему noexcept обязателен именно на move-версиях (см. грабли ниже — иначе std::vector тихо откатится на copy при реаллокации), и почему copy-присваивание через copy-and-swap безопаснее «ручного» варианта с delete[] data_; data_ = new int[...]; — в ручном варианте исключение из new оставляет объект в разрушенном состоянии (data_ уже удалён, новый не выделен), а copy-and-swap гарантирует, что *this не меняется, пока копия tmp не создана полностью успешно.What's usually probed further: why noexcept is mandatory specifically on the move versions (see gotchas below — otherwise std::vector silently falls back to copy on reallocation), and why copy assignment via copy-and-swap is safer than the "manual" approach with delete[] data_; data_ = new int[...]; — in the manual version, an exception from new leaves the object in a broken state (data_ already deleted, the new one not allocated), whereas copy-and-swap guarantees *this stays unchanged until the copy tmp has been fully and successfully constructed.
std::move(x) — просто static_cast<T&&>(x), приведение типа. Реальное перемещение происходит только если после этого вызывается move-конструктор/move-присваивание; для типа без move-семантики (например, тривиальный класс без указателей) std::move молча деградирует до обычного копирования.std::move(x) is just static_cast<T&&>(x), a type cast. The actual move happens only if a move constructor/move assignment operator is subsequently invoked; for a type without move semantics (say, a trivial class with no pointers), std::move silently degrades to an ordinary copy.
Стандарт гарантирует только «valid but unspecified state» для объекта после того, как он был источником move — обращение к его значению (не к его существованию) до присвоения нового значения — логическая ошибка, хоть и не всегда UB. Частый баг — обращение к переменной сразу после std::move(x) в том же выражении или следующей строке.The standard only guarantees a "valid but unspecified state" for an object after it has been the source of a move — accessing its value (not its existence) before assigning it a new value is a logic error, though not always UB. A common bug is touching a variable right after std::move(x) in the same expression or on the next line.
Если move-конструктор может кинуть исключение (не помечен noexcept), std::vector из соображений строгой exception safety при реаллокации использует copy-конструктор вместо move — тихая потеря производительности без единой ошибки компиляции.If the move constructor might throw (isn't marked noexcept), std::vector, for the sake of strict exception safety, uses the copy constructor instead of move during reallocation — a silent performance loss with no compiler error whatsoever.