Rvalue reference
&&— rvalue-ссылка (&— lvalue-ссылка).- Позволяет передавать в функцию
rvalue. - Продлевает жизнь временным объектам (так же, как обычная
const&). - Move constructor.
- Move assignment operator (
operator=(T&&)). - Reference collapsing.
int&& func(int&& i) {
// тут типа работаем с i как с lvalue
return i;
}
int main() {
int&& i = 1;
const int&& j = 2;
std::cout << func(1);
int x = 2;
int&& rx = x; // ERROR: x — lvalue, нельзя привязать к rvalue&&
const int&& crx = x; // ERROR: то же самое
return 0;
}
Важная тонкость: хотя i объявлен как int&&, внутри функции он используется как lvalue — у него есть имя, есть адрес. Категория «rvalue» относится к моменту передачи аргумента, а не к самому объекту в теле функции.
Правила выбора перегрузки:
- Если можно передать по обычной ссылке (
T&) — передаётся по ней (неконстантный аргумент к неконстантному параметру). - Если есть
rvalue-перегрузка —rvalueпередастся через неё. - Иначе
rvalueпередаётся по константной ссылке (const T&).
Move-конструктор и move-присваивание
- Принимают на вход
rvalue-reference. - Знаем, что источник нам больше не нужен — значит, можно «украсть» его внутренности.
- Например, для динамического массива — просто передать указатели на буфер, не копируя содержимое.
- Экономим на пересоздании объекта после знания, что оригинал больше не понадобится.
Что делает move:
- Передаёт значения полей в текущий объект.
- Оставляет источник в корректном, но неопределённом состоянии (так требует стандарт — над объектом ещё можно вызывать деструктор,
operator=и т.д., но нельзя предполагать, что в нём какие-то конкретные данные). - Очищает ресурсы текущего объекта.
default / delete, правило 5 (если определили один из специальных методов — обычно нужно определить все пять: деструктор, copy ctor, copy assign, move ctor, move assign) и правило 0 (если значения по умолчанию устраивают — не определяй ничего).
int main() {
CArray arr1{5};
CArray arr2{};
arr2 = arr1; // lvalue → copy assignment
arr2 = createArray(); // prvalue → move assignment
arr2 = std::move(arr1); // xvalue → move assignment
return 0;
}
std::move
- Делает из
lvalue—xvalue(eXpiring); при передаче в функцию этоrvalue. - Сам по себе ничего не «двигает» — просто кастует через
static_castкrvalue-ссылке. Реальное «движение» делает уже move-конструктор/оператор.
template <class _Tp>
typename remove_reference<_Tp>::type&&
move(_Tp&& __t) _NOEXCEPT {
typedef typename remove_reference<_Tp>::type _Up;
return static_cast<_Up&&>(__t);
}
- После
std::move(obj)самobjвсё ещё жив и валиден, но в неопределённом состоянии. Его можно дальше использовать (присваивать, разрушать), но нельзя предполагать, какие в нём данные.
Copy-and-swap для move
В операторе присваивания до этого делали копию для безопасности. С появлением move можно:
- Сделать один шаблон:
operator=(CArray other)— принимает по копии. Если передан lvalue — будет copy ctor; если rvalue — move ctor. Внутри просто свап. - Минус: даже при rvalue будет один лишний move-конструктор.
- Если эта цена устраивает — один оператор покрывает оба случая (copy-and-swap idiom).
Эффективный swap
Раньше — три копирования. С move — три перемещения:
template<typename T>
void std::swap(T& x, T& y) {
T tmp = std::move(x);
x = std::move(y);
y = std::move(tmp);
}
Forwarding reference (универсальная ссылка)
template<typename T>
void function(T&& value) {
// ...
}
int main(int, char**) {
Foo* foo = new Foo{};
auto&& value = foo;
}
- Это не rvalue-ссылка, а forwarding reference (универсальная). Срабатывает только когда
T&&стоит при шаблонном параметре, тип которого выводится; либо вauto&&. - Если передан
lvalue—Tвыводится какFoo&, иT&&после reference collapsing становитсяFoo&. - Если передан
rvalue—Tвыводится какFoo, иT&&остаётсяFoo&&.
Reference collapsing: компилятор «схлопывает» ссылки по правилам:
Foo& &→Foo&Foo&& &→Foo&Foo& &&→Foo&Foo&& &&→Foo&&
Проблема: внутри функции value — это lvalue (у него есть имя). Но мы хотим сохранить категорию (lvalue/rvalue) при передаче дальше:
std::move— безусловный каст к rvalue (плох: lvalue превратится в rvalue, его «украдут»).std::forward<T>(value)— условный каст:- если
T— lvalue-ссылка, оставляет lvalue; - если
T— обычный тип (бывший rvalue), кастует к rvalue.
- если
И std::move, и std::forward используют static_cast — это делается на этапе компиляции, без рантайм-затрат.
Copy elision
Механика, при которой компилятор избавляется от лишних копирований.
- RVO (Return Value Optimization): если функция возвращает значение, а вызывающий им инициализирует переменную, компилятор может конструировать объект сразу в памяти переменной — без вызова copy/move конструктора.
- NRVO (Named RVO): работает, даже если внутри функции у возвращаемого объекта было имя:
Foo f() { Foo result; // ... return result; // NRVO — result строится сразу в памяти получателя } - Там, где неоднозначно (например, тернарный оператор возвращает разные именованные объекты) — адрес объекта подставить нельзя, оптимизации не будет.
Резюме
std::move— снимает с типа ссылку и возвращает rvalue. Безусловный каст.std::forward— сохраняет исходную категорию аргумента (lvalue или rvalue).