До 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на перемещении.