Урок по треку C++ 6 мин

Move-семантика и perfect forwarding

Категории значений, rvalue-ссылки, std::move и std::forward, noexcept-перемещение, copy elision.

До C++11 вернуть из функции большой std::vector или положить строку в контейнер означало скопировать все данные. Move-семантика позволяет забрать ресурсы у объекта, который всё равно скоро умрёт: у вектора это три указателя, и перемещение стоит O(1) вместо O(n).

Категории значений

Каждое выражение имеет категорию:

  • lvalue: у объекта есть имя или адрес, он переживёт выражение (x, v[0], *p, вызов функции, возвращающей T&);
  • prvalue: «чистое» временное значение (42, a + b, std::string("hi"), вызов функции, возвращающей T);
  • xvalue: объект, ресурсы которого можно забрать (std::move(x), вызов функции, возвращающей T&&).

prvalue и xvalue вместе называются rvalue. Ключевой факт: именованная rvalue-ссылка сама является lvalue. Внутри void f(std::string&& s) выражение s это lvalue, и чтобы передать его дальше перемещением, снова нужен std::move(s).

rvalue-ссылки и перегрузка

void take(const std::string& s);   // принимает всё, копирует
void take(std::string&& s);        // выбирается для rvalue, может забрать буфер

Перемещающий конструктор забирает ресурс и оставляет источник в корректном, но неуказанном состоянии: его можно уничтожить или присвоить ему новое значение, но не стоит полагаться на содержимое.

Buffer(Buffer&& o) noexcept
    : data_(std::exchange(o.data_, nullptr)), size_(std::exchange(o.size_, 0)) {}

std::move ничего не перемещает

std::move(x) это просто static_cast<T&&>(x): приведение к xvalue. Перемещение делает конструктор или присваивание, который выбирается благодаря этому приведению. Отсюда ловушки:

  • std::move от const объекта даёт const T&&, а он связывается с копирующим конструктором: будет копия.
  • return std::move(local); мешает NRVO и обычно хуже, чем return local;. При возврате локальной переменной компилятор и так сначала пробует перемещение.

Copy elision

С C++17 при инициализации из prvalue того же типа объект создаётся сразу в месте назначения: T x = T(...), return T(...) не вызывают ни копирования, ни перемещения (гарантированный пропуск). Возврат именованной локальной переменной (NRVO) компиляторы тоже обычно оптимизируют, но это не гарантировано, поэтому у типа должен быть доступный конструктор копирования или перемещения.

Forwarding-ссылки и perfect forwarding

В шаблоне template <class T> void f(T&& x) параметр T&& не rvalue-ссылка, а forwarding-ссылка (универсальная): при lvalue-аргументе T выводится как U&, и по правилу схлопывания ссылок U& && превращается в U&. При rvalue T = U, и тип параметра U&&.

Правила схлопывания: если хоть одна ссылка lvalue, итог lvalue-ссылка; только && && даёт &&.

std::forward<T>(x) возвращает x той категории, с которой его передали: lvalue остаётся lvalue, rvalue снова становится rvalue. Так пишут обёртки, фабрики и emplace:

template <class... Args>
std::unique_ptr<Widget> make(Args&&... args) {
    return std::make_unique<Widget>(std::forward<Args>(args)...);
}

Правило: std::move для rvalue-ссылок (вы знаете, что объект можно забрать), std::forward для forwarding-ссылок (вы не знаете).

noexcept и контейнеры

При реаллокации std::vector перемещает элементы, только если перемещающий конструктор noexcept (или копирования нет). Иначе копирует, чтобы сохранить строгую гарантию исключений. Забыли noexcept: получили копирование всех элементов при росте.

Частые ошибки

  • Использовать объект после std::move так, будто он не изменился.
  • std::move на const объекте и удивление, почему копия.
  • std::move вместо std::forward в шаблоне: rvalue-обёртка «съедает» lvalue-аргументы вызывающего.
  • return std::move(x) для локальной переменной.
  • Перемещающий конструктор без noexcept.

Что спросят на собесе

  • Что делает std::move и в каком состоянии объект после перемещения.
  • lvalue, prvalue, xvalue на примерах; почему именованная T&& это lvalue.
  • Forwarding-ссылки, схлопывание ссылок, std::forward.
  • Copy elision и NRVO, почему не стоит писать return std::move(x).
  • Зачем noexcept на перемещении.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.