ПрограммированиеC++ разработчик

Что такое 'lvalue' и 'rvalue' в C++? Объясните их различия, способы их передачи в функции и зачем появились ссылки на rvalue (rvalue references)?

Проходите собеседования с ИИ помощником Hintsage

Ответ

В C++ lvalue (left value) — это выражение, ссылающееся на объект в памяти, у которого есть имя и на который можно ссылаться (например, переменная). rvalue (right value) — это временное значение, не имеющее имени, и не являющееся объектом в традиционном смысле (например, результат a + b, литералы типа 5).

Lvalue можно брать по адресу, а rvalue — нет (до появления rvalue-ссылок). Для передачи их в функции существуют:

  • Обычные ссылки: void foo(const std::string& s); — принимают lvalue и rvalue.
  • Lvalue-ссылки: void bar(std::string& s); — принимают только lvalue.
  • Rvalue-ссылки (C++11+): void baz(std::string&& s); — принимают только rvalue.

Пример:

void takeValue(std::string& s) { } // lvalue void takeRValue(std::string&& s) { } // rvalue std::string s = "hello"; takeValue(s); // OK, lvalue takeRValue(std::string("hi")); // OK, rvalue

rvalue-ссылки нужны для эффективной передачи временных объектов, главным образом для move-семантики, чтобы перемещать ресурсы, а не копировать их.

Вопрос с подвохом

Какой тип ссылки (lvalue или rvalue) получит выражение std::move(obj)? Какой категории станет сам объект после применения std::move?

Ответ:

std::move(obj) всегда возвращает rvalue-ссылку (T&&), но сам объект остаётся lvalue, просто к нему применяется явное преобразование. После этого с объектом надо обращаться крайне осторожно (он может быть в неопределённом, но валидном состоянии).

Пример:

std::string s = "data"; std::string d = std::move(s); // d получает данные s, s теперь пустой

Примеры реальных ошибок из-за незнания тонкостей темы.


История

В крупном проекте один из разработчиков передавал временные объекты через lvalue-ссылку T& (вместо T&& или const T&). Это приводило к компиляционным ошибкам и неоптимальным копированиям — move-семантика не использовалась, производительность снизилась на 40%.


История

Во фронтенд-движке неверно применяли std::move к переменным, которые после этого использовали снова. Из-за этого переменные были в "разрушенном" состоянии, вызывались аварийные завершения и крашились рендер-потоки.


История

В библиотеке сериализации передавали контейнер типа std::vector<T> в функцию как lvalue, а ожидали move. Вместо перемещения шло дорогое копирование большого количества элементов, что резко ухудшило время сериализации на больших массивах.