Rvalue Reference

среда, апр. 9, 2025 | 4 минуты чтения

Rvalue Reference

2 семестр
Основы программирования.

Rvalue reference

  • && - rvalue reference (& - lvalue reference)
  • позволяет передавать в функцию rvalue
  • продлевает жизнь временным объектам (так же как и обычная)
  • move constructor
  • move assignment operator (operator=(&&))
  • reference collapsing

пример

int&& func(int&& i) {
	 // тут типо работаем с i как с lvalue reference
	return i; // а тут типо возвращаем копию, которая rvalue 
}

int main() {
	int&& i = 1;
	const int&& j = 2;
	std::cout << func(1);
	
	int x = 2;
	int&& rx = x; // error
	const int&& crx = x; // error
	return 0;
}
  • по переменной х конкретно понимаем в каком месте памяти лежит переменная. Есть идентика
  • rvalue-ссылка позволяет передать rvalue в выражение, но сам по себе аргумент переданный как rvalue-ссылка. Но в тот момент, когда мы передали rvalue по rvalue-ссылке стал lvalue.
  • Если можно передать по обычной ссылке, то передаётся по простой ссылке (неконстантный в неконстанту)
  • rvalue передаётся по rvalue, если есть rvalue-перегрузка. Если нет, то по константной ссылке.

move-constructor and move assigment

  • принимают на вход rvalue-reference
  • знаем, что он нам больше не нужен
  • например, для массива можно просто передать указатели на начало.
  • Экономим на пересоздании объекта после знания о том, что он нам больше не понадобится
  • передают все значения полей в текущий объект
  • Оставляет копируемы объект в инвариантном но неопределённом состоянии
  • очищают ресурсы текущего объекта
  • default/delete
  • Правило 5
  • Правило 0 (если всё по умолчанию удовлетворяют. Для move - это чисто swap-ы)
int main() {
	CArray arr1{5};
	CArray arr2{};
	arr2 = arr1; // lvalue
	arr2 = createArray(); // prvalue
	arr2 = std::move(arr1); // xvalue
	return 0;
}
  • std::move делает из lvalue - xvalue, при передаче в функцию будет rvalue
  • просто кастит статиком к rvalue-ссылке
template <class _Tp>
typename remove_reference<_Tp>::type&&
move(_Tp&& __t) _NOEXCEPT {
	typedef typename remove_reference<_Tp>::type _Up;
	return static_cast<_Up&&>(__t);
}
  • lvalue-переменная после move-копирования(std::move) всё ещё жива и валидна. но в неопределённом состоянии -> её можно дальше использовать

  • в операторе присваивания до этого делали копию для безопасности

  • в assign операторе теперь просто Swap

    • (copy-and-swap idiom) однако оператор присваивания и assign-оператор очень похожи, поэтому можно сделать один оператор присваивания, в который будем передавать объект по копии
    • если готовы пожертвовать тем, что при копировании в функцию будет вызван move-constructor, то можем делать один и тот же оператор. Иначе - пишем 2 разных (кажется, что неверно написано объяснение… В примере было вызвано 2 дефолтных конструктора)
  • Эффективный swap-трижды сделать move вместо копирований

template<typename T>
void std::swap(T& x, T& y)
	T tmp = move(x);
	x = move(y);
	y = move(tmp);
}

Forwarding reference

template<typename T>
void function(T&& value) {
}

int main(int, char**) {
	Foo** foo = Foo{};
	auto && value = foo;
}
  • универсальная ссылка

  • Если у нас шаблон Т и T&&, то это не rvalue, а forwarding

    • пойдёт по lvalue, если Т-lvalue

    • пойдёт по rvalue, если T-rvalue

    • В момент компиляции понимает на что ему заменить Т (дописать)

    • Foo& & -> Foo&

    • Foo&& & -> Foo&

    • Foo& && -> Foo&

    • Foo&& && -> Foo&&

    • Если создаём что-то с универсальной ссылкой, то оно становится lvalue объектом и нам дальше придётся кидать как lvalue&, но нам не всегда хочется это делать. (move сделать тоже не можем, потому что lvalue объект мы не хотим мувать)

      • помогает std::forward
      • lvalue скастит к lvalue
      • rvalue скастит к rvalue
      • в отличии от std::move, который делает это безусловно
  • в move и forward static-cast используется по делу, поэтому можно не переживать (+делается на этапе компиляции)

copy elision

  • механика, при которой существует избавление от лишних копирований. (см пример со слайда

  • К примеру если возвращается что-то, чем мы потом будем инициализировать переменную, то можно сразу передать адрес объект к тому, что мы будем инициализировать, тогда не будет лишних конструкторов) (rvo)

  • даже если мы не сразу передаём по копии, а сначала конструируем в теле функции, то также происходит оптимизация(nrvo)

  • там, где неоднозначно(к примеру тернарный оператор при возвращении), то адрес объекта подставить нет возможности и оптимизации не будет

  • std::move - снимает с типа какую-либо ссылку или копию, то возвращает rvalue

  • std::forward - оставляет тот же самый тип