Лекция 10

09.04.2025 Обновлено: 09.04.2025

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

  • Делает из lvaluexvalue (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&&.
  • Если передан lvalueT выводится как Foo&, и T&& после reference collapsing становится Foo&.
  • Если передан rvalueT выводится как 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).