Отсебятина

Оригинальный заголовок: lvalues, rvalues, glvalues, prvalues, xvalues, help! Хабр не разрешает поставить восклицательный знак в конце заголовка.

Случайно попалась эта довольно старая статья 2018 года с простым и понятным описанием категорий значений в C++. До неё всякие glvalue, prvalue, xvalue были малопонятными для меня.

cppreference.com просто перечисляет категории, и это не добавляет понимания, всё кажется чрезмерно излишним.

На stackoverflow.com есть 24 поста разной степени ценности, что только добавляет недоумения от сложности этой темы. Там уже есть картинки, которые призваны упростить понимание, например такие:

Привыкли к lvalue и rvalue, а здесь оказывается, что они вообще рядом не стояли
Привыкли к lvalue и rvalue, а здесь оказывается, что они вообще рядом не стояли
Здесь с привычными примерами может быть чуточку понятней
Здесь с привычными примерами может быть чуточку понятней

Но это всё довольно быстро забывается, продолжаешь пользоваться привычными lvalue и rvalue, и всякие гуру на stackoverflow минусуют тебя, если ты неправильно назвал rvalue вместо prvalue, или пишут комментарии или свои ответы, которые может быть и не лучше твоих, но публика любит плюсовать те ответы, которые используют малопонятные сложные термины, демонстрирующие превосходство знаний.

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

Предлагаемая статья очень лёгкая, помогла мне понять, что такое сложное устройство - необходимое, совсем не сложное и легко запоминается.

Собственно перевод

Вы уже привыкли к интуитивному определению «lvalue» и «rvalue», но всё ещё путаетесь насчёт glvalue, xvalue и prvalue и тревожитесь из‑за того, что lvalue и rvalue могут меняться? Цель этой статьи — развить вашу интуицию на все эти пять категорий.

Предупреждаю, эта статья не стремится дать полное определение этим пяти категориям значений. Вместо этого, я надеюсь научить вас интуитивно понимать их, когда потребуются детали о них.

Были две категории до C++11 — lvalue и rvalue. Они интуитивно понятны, lvalue — это что‑то, имеющее имя, такие, как переменные, и rvalue — это что‑то временное:

Потом появились rvalue-ссылки и семантика перемещения. На первый взгляд, старых lvalue / rvalue всё ещё достаточно: нельзя перемещать lvalue (они могут использоваться позже), можно перемещать rvalue (они же временные):

Почему я выделяюкрасным «Нельзя переместить» и «lvalue»? Ведь может оказаться, что вы хотите переместить некоторые lvalue! Например, у вас есть lvalue, который вы больше не хотите использовать, вы можете привести его к rvalue-сслыке с помощью std::move() . Любая функция тоже может вернуть rvalue-ссылку на объект с именем.

То есть получается, что-то, что имеет имя и что‑то, что может быть перемещено — ортогональны, не связаны между собой. Мы вскоре решим проблему перемещения lvalue, но сейчас давайте изменим нашу диаграмму, чтобы отобразить ортогональный вид этого мира:

Ясно, что чего-то не хватает в левом нижнем углу. (Мы можем игнорировать правый верхний угол, потому что временные объекты, которые не могут быть перемещены - бесполезная концепция.)

C++11 представил новую категорию «xvalue» для lvalue, которые могут быть перемещены. Полезно думать об «xvalue» как «eXpiring lvalue» («умирающее lvalue»), потому что они, вероятно, заканчивают свои жизни и готовы к перемещению (например, rvalue‑ссылка из функции).

Дополнительно, то, что раньше называлось «rvalue», было переименовано в «prvalue», что значит «pure rvalue» («чистое rvalue»). Это три основные категории:

Но мы ещё не пришли к тому, что же такое «glvalue», и что такое теперь «rvalue». Кажется, будто мы уже объяснили эти концепты! Просто ещё не да ли им правильных имён и не нарисовали.

glvalue, или «generalized lvalue» («обобщённое lvalue»), в точности покрывает всё, что «имеет имя», игнорируя перемещаемость. rvalue покрывает всё, что может быть перемещено, игнорируя имя. И это всё! Теперь вы знаете все 5 категорий значений.

Если вам понравился этот пост, вы можете подписаться на блоги автора или следить за его Twitter.

Комментарии (8)


  1. NeoCode
    26.12.2024 05:25

    Отличная статья! А еще есть perfect forwarding, который тоже может сбить с толку. Потому что разработчики языка решили сэкономить и использовали оператор && не только для семантики перемещения, но и для еще одной цели - когда компилятор сам выбирает способ передачи, по значению или по ссылке, но только в шаблонах:)


    1. sergio_nsk Автор
      26.12.2024 05:25

      На самом деле это довольно легко. Если параметр функции - это T&& , и T - это параметр шаблона функции, то применяется folding, который и есть ключ для perfect forwarding - вычёркивание двойных &&, если их больше 2: аргумент int a имеет тип int& , и T&& становится int&&& , что есть int& && - int& после свёртки. А std::move(a) имеет тип int&& и T&& становится int&&&&, что есть int&& && - int&& .


  1. brotchen
    26.12.2024 05:25

    Не могу понять, что такое, имеющее имя, но которое нельзя переместить. Можно пример кода, где появляется " например, rvalue‑ссылка из функции"?


    1. 26rus_mri
      26.12.2024 05:25

      по умолчанию переменная не перемещается а копируется.

      void foo(string str) {...}
      ...
      string str;
      foo(str);

      с таки кодом внутри foo будет доступ к независимой копии str

      а вот, если сделать так: foo(std::move(str));
      копия не будет создана, содержимое строки будет перемещено в функцию, а str снаружи станет пустой

      rvalue‑ссылка из функции

      не совсем понял что имеется ввиду, возможно что-то такое

      struct A{
      string str;
      string&& extract() { return str; }
      };


      1. sergio_nsk Автор
        26.12.2024 05:25

        содержимое строки будет перемещено в функцию, а str снаружи станет пустой

        Не совсем верно без деталей функции. Короткие строки будут скопированы. Для длинных строк тоже есть варианты. Если параметр функции - ссылка, то ничего не произойдёт, если rvalue-ссылка, то ничего, перемещение или копирование зависят от тела функции, если не ссылка, то строка переместится в аргумент функции.


        1. 26rus_mri
          26.12.2024 05:25

          Короткие строки будут скопированы. Для длинных строк тоже есть варианты.

          Я говорю о строках из стандартной библиотеки мейнстримных компиляторов. У них, насколько мне известно, нет никаких вариантов, всё строго и однозначно.

           Если параметр функции - ссылка

          так а я привел прототип там параметр передается по значению


    1. sergio_nsk Автор
      26.12.2024 05:25

      пример кода, где появляется " например, rvalue‑ссылка из функции"

      std::move(x) возвращает rvalue-ссылку на x.


  1. TheDreamsWind
    26.12.2024 05:25

    Для себя использую такую шпаргалку:

    • lvalue - любое выражение, чей тип является lvalue ссылкой или которое является именованой переменной.

    • xvalue - любое выражение, тип которого является rvalue ссылкой, за исключением именованой переменной.

    • prvalue - любое выражение, тип которого не является ссылкой и которое не является именованой переменной.