C++MiddleCoding

Что такое 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, ограничения и диагностируемые симптомы ошибки.

Sources

Related topics