Отсебятина

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

Случайно попалась эта довольно старая статья 2018 года и дополненная в 2019 году с простым и понятным описанием категорий значений в 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 — это выражения, вычисляющие временные объекты (без имени). Рассмотрим эти определения:

  Widget w;
  Widget getWidget();

Если мы используем выражение w , его результат вывода - это объект w , который имеет имя. Если мы используем выражение getWidget() , его результат вызова - это временный объект без имени. Попробуем иллюстрировать это так:

Потом появились 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 категорий значений.

Если вы хотите погрузиться ещё глубже в эту тему, на cppreference есть очень хорошая статья.

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

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


  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. alan008
        26.12.2024 05:25

        Прочитав это, я радуюсь, что никогда не писал продуктовый код на C++ (и надеюсь не буду) :-)


    1. Dooez
      26.12.2024 05:25

      Perfect forwarding никогда не будет передачей по значению, всегда только по ссылке.

      И это не совсем специальный синтаксис, а результат взаимодействия reference collapsing с дедукцией типов. Правила дедукции может и специальные, но как мне кажется нельзя сказать что кто-то сэкономил.


  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

            Когда я писал ответ, прототипа функции не было.

            нет никаких вариантов, всё строго и однозначно.

            Ничего однозначного нет. После перемещения строка находится в неопределёном состоянии.

            Все известные реализации стандартной библиотеки имеют оптимизации для коротких строк, потому что имея много байтов на стеке для указателя + размера + ёмкости, выделять ещё 1 нулевой байт на куче для пустой строки - это расточительство. Вот на русском https://pvs-studio.ru/ru/blog/terms/6658/


          1. rsashka
            26.12.2024 05:25

            https://devblogs.microsoft.com/oldnewthing/20230803-00/?p=108532
            https://devblogs.microsoft.com/oldnewthing/20240510-00/?p=109742

            Сравнение реализации строк из мейнстримных компиляторов (ms, gcc и clang)


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

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

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


    1. Dooez
      26.12.2024 05:25

      "нельзя переместить" означает что не произойдет связывание с аргументом типа revalue reference. То есть не вызовется конструктор перемещения, например.


  1. TheDreamsWind
    26.12.2024 05:25

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

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

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

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


  1. JediPhilosopher
    26.12.2024 05:25

    Как же я люблю заходить в эти статьи про текущее состояние С++ и охреневать с того, какой еще вырвиглазно сложной неочевидной мути намудрили в этом языке. И радоваться что уже больше десяти лет на нем не пишу, и скорее всего никогда уже не буду. И мне не надо изучать всё это метапрограммирование на шаблонах и вот эти непонятные штуки.

    Это ппц какой-то, будто сговорились напихать в него самых каких-то сложных и неочевидных концепций. Что это, зачем это всё, кто все эти люди? Как другие языки без этого всего живут?


    1. Arenoros
      26.12.2024 05:25

      а чему собсвенно радоваться? по мне это тоже самое что радоватсья отсутствию необходимости обувь покупать и шнурки завязывать когда ноги отпилили. На C++ можно просто не пользоваться тем чего не понимаешь.