move semantics

std::move + move-конструкторыstd::move + move constructors

Явная передача владения ресурсом объекта вместо его копирования. 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.

архитектурный сдвиг стандартаan architectural shift in the standard объект после move — valid but unspecifiedobject after move — valid but unspecified частый вопрос: напиши move-конструктор для своего классаcommon interview question: write a move constructor for your class

Что этоWhat it is

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.

до C++11 — только копияbefore C++11 — copy only
std::vector<std::string> a = {"x", "y"};
std::vector<std::string> b = a;  // глубокая копия всех строк// a deep copy of every string
с movewith move
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

Зачем ввелиWhy it was introduced

Move semantics устраняет системную неэффективность, встроенную в язык с самого начала:Move semantics eliminates a systemic inefficiency that had been built into the language from the start:

01

Копирование временных объектов было расточительным по умолчаниюCopying temporaries was wasteful by default

До 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.

02

Контейнеры при реаллокации копировали все элементыContainers copied every element on reallocation

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.

03

Move-only типы стали возможныMove-only types became possible

Без 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.

Как это работает под капотомHow it works under the hood

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").

Своя реализацияImplementing it yourself

rule of five — то, что просят дописать целикомrule of five — what interviewers ask you to complete in full

Вариант вопроса на интервью — не «объясни 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.

ГраблиGotchas

std::move ничего не перемещает сам по себеstd::move doesn't move anything by itself

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.

Использование объекта после move — не UB, но состояние не определеноUsing an object after a move isn't UB, but its state is unspecified

Стандарт гарантирует только «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 при реаллокацииA move constructor without noexcept isn't used by std::vector on reallocation

Если 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.