Кто есть кто в C++: lvalue, rvalue, xvalue и другие «значения»

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что скрывается за буквами l, r, x, gl и pr? Разбираемся с категориями значений в C++

Если вы когда-нибудь писали на C++ код и получали ошибку вроде "cannot bind non-const lvalue reference to an rvalue", то вы уже сталкивались с этой «магией». Категории значений — lvalue, rvalue, xvalue, glvalue и prvalue — это не просто академическая классификация. Это фундамент, на котором держится семантика перемещения (move semantics) и идеальная передача (perfect forwarding).

Давайте раз и навсегда разберемся, что означает каждая буква, как их различать на практике и почему без этого знания невозможно писать современный эффективный C++.

Краткая история: откуда взялись эти пять терминов?

Изначально, ещё во времена C, было всего два понятия: lvalue (левое значение — то, что может стоять слева от присваивания) и rvalue (правое значение — то, что стоит справа). Всё было просто: переменная — lvalue, число — rvalue.

С появлением C++11 и семантики перемещения эта простая картина перестала работать. Появились ссылки на rvalue (&&) и необходимость различать «умирающие» временные объекты. Так родилась новая классификация, которую стандарт C++ описывает через пять категорий.

Взгляните на иерархию (диаграмма в виде текста):

        expression
        /        \
   glvalue      rvalue
   /      \    /      \
lvalue   xvalue   prvalue

Запомните эту картинку — это ключ ко всему.

lvalue (left value) — «живучие» объекты

lvalue — это выражение, которое ссылается на объект, имеющий адрес в памяти. Такой объект не исчезнет после окончания выражения. Вы можете взять его адрес, изменить его, и он продолжит существовать.

Примеры lvalue:

  • Имя переменной: int x = 5; x — это lvalue.
  • Разыменованный указатель: *ptr.
  • Ссылка на переменную: int& ref = x; ref.
  • Строковый литерал: "hello" (да, это lvalue, хотя и константное).

Код:

int main() {
    int a = 10;
    int* p = &a;   // Взятие адреса — ок, a — lvalue
    int& ref = a;  // Ссылка на lvalue

    // &(a + 1);   // Ошибка! a+1 — rvalue, адреса нет
    return 0;
}

rvalue (right value) — «одноразовые» значения

rvalue — это выражение, которое не имеет постоянного адреса в памяти. Оно существует только в момент вычисления выражения. Вы не можете взять его адрес (за редкими исключениями).

К rvalue относятся:

  • Литералы (кроме строковых): 42, 3.14f, true.
  • Результаты арифметических операций: a + b.
  • Временные объекты: std::string("temp").
  • Лямбда-выражения.

Код:

int foo() { return 5; }

int main() {
    int x = 42;       // 42 — rvalue
    int y = x + 10;   // x+10 — rvalue
    // int* p = &(x + 10); // Ошибка! Нельзя взять адрес rvalue
    return 0;
}

prvalue (pure rvalue) — «чистые» rvalue

prvalue — это классическое rvalue из C++98. Это временное значение, которое не имеет имени и не привязано к памяти. Именно prvalue мы обычно и называем rvalue в разговорной речи.

Примеры prvalue:

  • Числа, перечисления, результаты операторов.
  • Вызов функции, возвращающей не-ссылку: std::to_string(123).
  • Временный объект, созданный явно: MyClass().

xvalue (expiring value) — «умирающие» объекты

Вот тут начинается самое интересное. xvalue — это объект, который скоро будет уничтожен, но его ресурсы ещё можно «спасти» (переместить). Это мост между lvalue и rvalue.

Типичный способ получить xvalue — использовать std::move() или explicit cast к rvalue-ссылке.

Код:

#include <string>
#include <utility>

int main() {
    std::string s = "Hello, world!";
    std::string&& ref = std::move(s);  // std::move(s) — xvalue!
    // Теперь ref — это rvalue-ссылка, s — в перемещённом состоянии
    return 0;
}

Важно: std::move(s) не перемещает данные. Он просто создаёт xvalue — «метку», которая говорит компилятору: «Эй, этот объект можно разобрать на запчасти». А уже конструктор перемещения или оператор присваивания решают, что делать.

glvalue (generalized lvalue) — обобщённое левое значение

glvalue — это объединение lvalue и xvalue. Простыми словами, это любое выражение, которое имеет идентичность (адрес). Независимо от того, умрёт оно завтра или нет, glvalue — это то, на что можно ссылаться по адресу.

Все lvalue — это glvalue. Все xvalue — тоже glvalue. А вот prvalue — это не glvalue.

Практика: как это работает в коде? Разбор типичных примеров

Пример 1. Перегрузка функций по категориям

#include <iostream>

void process(int& x) {
    std::cout << "lvalue: " << x << std::endl;
}

void process(int&& x) {
    std::cout << "rvalue: " << x << std::endl;
}

int main() {
    int a = 10;
    process(a);        // lvalue → вызов process(int&)
    process(20);       // prvalue → вызов process(int&&)
    process(std::move(a)); // xvalue → вызов process(int&&)
    return 0;
}

Вывод:

lvalue: 10
rvalue: 20
rvalue: 10

Обратите внимание: std::move(a) превратило lvalue a в xvalue, и компилятор выбрал версию с rvalue-ссылкой.

Пример 2. Ошибка новичка: rvalue-ссылка как lvalue

<                

Блоги

Рекомендаций по книгам