До прихода C++11

  1. lvalue - это то, что может стоять слева от оператора присваивания.
  2. rvalue - это "временные объекты", им нельзя что-то присваивать. Кажется, что у них нельзя было взять адрес.

С приходом C++11 появилась move-семантика, из-за чего схема стала древовидной и усложнилась в понимании.

Сейчас

Введем пару понятий:

  • Наличие идентичности (identity) – наличие какого-то параметра, по которому можно понять, ссылаются ли два выражения на одну и ту же сущность (например, адрес в памяти)
  • Возможность перемещения (можно ли объект переместить, помувать)

Выражения сейчас делятся на два больших типа:

  • glvalue - обладают идентичностью
  • rvalue - могут быть перемещены

Эти категории распадаются в сумме на три (одна у них общая).

  1. glvalue

    • lvalue - обладают идентичностью, но не могут быть перемещены
    • xvalue - обладают идентичностью, могут быть перемещены
  2. rvalue

    • xvalue - то же самое
    • prvalue - не обладают идентичностью, могут быть перемещены

Да кто такие эти ваши glvalue и rvalue

Свойства glvalue:

  • Могут быть неявно преобразованы в prvalue
  • Могут быть полиморфными (?)
  • Не могут иметь тип void (из-за наличия идентичности)
  • Может быть неполным типом (incomplete type), где это разрешено выражением

Свойства rvalue:

  • Нельзя взять адрес оператором & (из-за отсутствия идентичности)
  • Не могут находиться слева от оператора =
  • Могут использоваться для инициализации const lvalue-ссылки или rvalue-ссылки, при этом время жизни объекта расширяется до времени жизни ссылки
  • Если в overload resolution пришли две функции, одна из которых принимает const-lvalue-ссылку, а другая rvalue-ссылку, то выберется вторая (то есть, например, если move-конструктор определен, то он предпочтительнее)

Про lvalue

Свойства:

  • Все свойства glvalue
  • Можно взять адрес оператором &
  • Модифицируемые lvalue могут стоять слева от оператора присваивания
  • Могут использоваться для инициализации lvalue-ссылки

Примеры lvalue:

  • Имя переменной, функции или поле класса любого типа. Даже если переменная является rvalue-ссылкой, имя этой переменной в выражении является lvalue (другими словами, именованная rvalue-ссылка является lvalue-значением, но это все еще rvalue-ссылка)
  • Вызов функции или оператора, возвращающего lvalue-ссылку
  • Преобразование к типу lvalue-ссылки
  • Результат встроенных операторов присваивания, составных операторов присваивания (=, +=, /= и т.д.), встроенных преинкремента и предекремента, встроенных операторов разыменования указателя
  • Результат встроенного оператора обращения по индексу от lvalue-массива
  • Строковый литерал, например, "Hello world!" (можно взять его адрес тоже)

Про prvalue

Свойства:

  • Все свойства rvalue
  • Не могут быть полиморфными (?)
  • Не могут быть неполного типа
  • Не могут иметь абстрактный тип или быть массивом элементов абстрактного типа

Примеры prvalue:

  • Литерал (кроме строкового), например, 42, false или nullptr
  • Вызов функции или оператора, который возвращает не ссылку
  • Результат преобразования к нессылочному типу
  • Результат встроенных операторов постинкремента и постдекремента, встроенных математических, логических операторов, операторов сравнения, взятия адреса
  • Указатель this
  • Элемент перечисления (enum)
  • Нетиповой параметр шаблона, если он не является классом
  • Лямбда-выражение (пример, [](int x) { return x*x; })

Про xvalue

Свойства:

  • Все свойства glvalue
  • Все свойства rvalue

Примеры xvalue:

  • Вызов функции или встроенного оператора, возвращающего rvalue-ссылку, например std::move(x)
  • Результат преобразования к rvalue-ссылке
  • Нестатический член класса от rvalue-объекта

std::move()

Функция std::move() не выполняет никаких перемещений, о чем порой заблуждаются - это просто приведение lvalue-аргумента к rvalue-ссылке.

Может быть реализована следующим образом:

template <class T>
constexpr remove_reference_t<T>&& move(T&& arg) noexcept {
    return static_cast<remove_reference_t<T>&&>(arg);
}

То есть это просто обертка над static_cast, которая «убирает» ссылку у переданного аргумента с помощью remove_reference_t и, добавив &&, преобразует тип в rvalue-ссылку.

В примере выше T&& - не rvalue-ссылка, а универсальная ссылка, о чем будет сказано в отдельном разделе.

Еще одна типичная ошибка при использовании std::move():

std::string get_my_string(const size_t index) {
    std::string my_string;

    // *do something*

    return std::move(my_string); // wrong!
}

Работать это будет, но это неэффективно. Лучше писать без std::move() - копирования не случится из-за оптимизации RVO и его друзей (когда они возможны).


Заимствования:

Категории выражений в C++ / Хабр (habr.com)

std::move vs. std::forward / Хабр (habr.com)