О том, как распознать ссылку Rvalue или Lvalue и правило if-it-have-a-name

Я читал статью Томаса Беккера о ссылках на rvalue и их использовании. Там он определяет, что он называет правилом if-it-has-a-name :

Вещи, объявленные как ссылка rvalue, могут быть lvalues ​​или rvalues. Отличительный критерий: если он имеет имя, то это значение lvalue. В противном случае это значение r.

Это звучит очень разумно для меня. Он также четко определяет справедливость ссылки на rvalue.

Мои вопросы:

  1. Согласны ли вы с этим правилом? Если нет, можете ли вы привести пример, где это правило может быть нарушено?
  2. Если нарушений этого правила нет. Можем ли мы использовать это правило для определения rvalueness / lvaluness выражения?

Это один из самых распространенных «эмпирических правил», используемых для объяснения того, что является разницей между значениями lvalues ​​и rvalues.

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

Сначала было C

Во-первых, что означают «lvalue» и «rvalue» первоначально, в мире языков программирования вообще?

На более простом языке, таком как C или Pascal, используются термины, используемые для обозначения того, что может быть помещено на L или e R или на оператор присваивания.

На языке, таком как Pascal, где присваивание не является выражением, а только выражением, разница довольно понятна и определяется в грамматических терминах. Lvalue – это имя переменной или индекс массива.

Это потому, что только эти две вещи могут стоять слева от задания:

 i := 42; (* ok *) a[i] := 42; (* ok *) 42 := 42; (* no sense *) 

В C применяется то же самое различие, и оно по-прежнему довольно грамматическое в том смысле, что вы можете посмотреть на строку кода и сказать, будет ли выражение выражать lvalue или rvalue.

 i = 42; // ok, a variable *p = 42; // ok, a pointer dereference a[i] = 42; // ok, a subscript (which is a pointer dereference anyway) s->var = 42; // ok, a struct member access 

Итак, что изменилось на C ++?

Маленькие языки растут

В C ++ вещи становятся намного более сложными, и разница больше не является грамматической, а включает процесс проверки типов по двум причинам:

  • Все может оставаться слева от задания, если его тип имеет соответствующую перегрузку operator=
  • Рекомендации

Таким образом, это означает, что в C ++ вы не можете сказать, будет ли выражение выражать lvalue только при просмотре его грамматической структуры. Например:

 f() = g(); 

это утверждение, которое не имеет смысла в C, но может быть совершенно законным в C ++, если, например, f() возвращает ссылку. Вот как работают выражения, такие как v[i] = j для std::vector : operator[] возвращает ссылку на элемент, поэтому вы можете назначить ему.

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

Это потому, что вы не хотите иметь юридический код:

 int &x = 42; x = 0; // Have we changed the meaning of a natural number?? 

Таким образом, язык тщательно определяет, что такое lvalue, а что нет, а затем говорит, что только lvalues ​​могут быть привязаны к неконстантным ссылкам. Таким образом, приведенный выше код не является законным, потому что целочисленный литерал не является значением lvalue, поэтому привязка к нему не может быть привязана к нему.

Обратите внимание, что ссылки на константы различны, поскольку они могут связываться с литералами и временными (и локальные ссылки даже продлевают срок жизни этих временных рядов):

 int const&x = 42; // It's ok 

И до сих пор мы только коснулись того, что уже имело место в C ++ 98. Правила были уже более сложными, чем «если у него есть имя, это значение lvalue», так как вам нужно рассмотреть ссылки. Таким образом, выражение, возвращающее неконстантную ссылку, все еще считается lvalue.

Кроме того, другие эмпирические правила, упомянутые здесь, уже не работают во всех случаях. Например, «если вы можете принять его адрес, это значение lvalue». Если «принимая адрес» вы имеете в виду «применение operator& », тогда это может сработать, но не обманывайте себя, думая, что вы никогда не сможете получить адрес временного: this указатель внутри временной функции-члена , например, укажет на это.

Что изменилось на C ++ 11

C ++ 11 ставит большую сложность в bin, добавляя понятие ссылки rvalue , то есть ссылку, которая может быть привязана к rvalue, даже если не const. Тот факт, что он может быть применен только к rvalue, делает его безопасным и полезным. Я не думаю, что это необходимо для объяснения того, почему ссылка rvalue полезна, поэтому продолжайте.

Дело здесь в том, что сейчас у нас есть намного больше дел, чтобы рассмотреть. Итак, что такое rvalue? Стандарт фактически различает различные типы r, чтобы иметь возможность правильно определять поведение ссылок rvalue и разрешения перегрузки и вычитания аргумента шаблона при наличии ссылок rvalue. Таким образом, мы имеем такие термины, как xvalue , prvalue и т. xvalue , prvalue делают вещи более сложными.

Как насчет наших эмпирических правил?

Таким образом, «все, что имеет имя, является значением lvalue», все равно может быть истинным, но наверняка это не так, что каждое lvalue имеет имя. Функция, возвращающая ссылку на константу lvalue, является значением lvalue. Функция, возвращающая что-то по значению, создает временную и представляет собой rvalue, поэтому функция возвращает ссылку rvalue.

Как насчет «временные значения». Это правда, но и не временные могут быть превращены в rvalues, просто забрасывая тип (как и std::move ).

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

Хотя правило охватывает большинство дел, я не могу согласиться с ним в целом:

Разыменование анонимного указателя не имеет имени, но это значение lvalue:

 foo(*new X); // Not allowed if foo expects an rvalue reference (example of the article) 

Основываясь на стандарте и принимая во внимание особые случаи временных объектов, являющихся значениями r, я предлагаю обновить второе предложение правила:

«… Критерий: если он обозначает функцию или объект, который не имеет временного характера, то это значение l …».

Вопрос 1: Это правило строго относится к classификации выражений ссылочного типа rvalue , а не к выражениям вообще. Я почти согласен с этим в этом контексте («почти», потому что это немного больше, см. Цитату ниже). Точная формулировка содержится в примечании к Стандарту [пункт 5 статьи 5]:

В общем, эффект этого правила заключается в том, что названные ссылки rvalue рассматриваются как lvalues, а неназванные ссылки rvalue на объекты рассматриваются как xvalues; Ссылки rvalue на функции обрабатываются как lvalues, имена или нет.

(подчеркивает мою, по очевидным причинам)


Вопрос 2: Как вы можете видеть из других ответов и комментариев (некоторые интересные примеры там), есть проблемы с общими, сжатыми утверждениями о категории значений выражения. Вот как я об этом думаю.

Нам нужно взглянуть на проблему с другой стороны: вместо того, чтобы указывать, какие выражения являются lvalues, укажите типы rvalues; lvalues ​​- это все остальное .

Во-первых, несколько определений, чтобы все было ясно:

  • Объект означает область хранения данных, а не функцию, а не ссылку (это определение в стандарте).
  • Когда я говорю, что выражение генерирует что-то, я имею в виду, что он не просто называет его или ссылается на него, а фактически строит и возвращает его как результат комбинации операторов, вызовов функций (возможно, вызовов конструктора) или отбрасываний (возможно, неявных отбрасываний ).

Теперь, основываясь главным образом на [3.10] (но также и на множестве других мест в стандарте), выражение является rvalue тогда и только тогда, когда оно является одним из следующих:

  1. значение , которое не связано с объектом (например, this или литералы типа 7 , а не строковые);
  2. выражение, которое генерирует объект по значению, например, временный объект ;
  3. выражение, которое генерирует ссылку rvalue для объекта ;
  4. рекурсивно, одно из следующих выражений с использованием значения r:
    • xy , где x – значение r, а y – нестатический объект -член;
    • x.*y , где x – значение r, а y – указатель на объект- член;
    • x[y] , где либо x либо y является значением r типа массива (используя встроенный оператор [] ).

Вот и все.

Технически, следующие специальные случаи также являются значениями, но я не думаю, что они актуальны на практике:

  1. вызов функции, возвращающий void , cast to void или throw (очевидно, не lvalues, я не уверен, почему меня когда-либо интересовала их категория ценности на практике);
  2. один из obj.mf , ptr->mf , obj.*pmf или ptr->*pmf ( mf – нестатическая функция-член, pmf – указатель на функцию-член); здесь мы говорим строго об этих формах, а не о вызовах функций, которые могут быть построены с ними, и вы действительно ничего не можете с ними сделать, кроме как вызвать вызов функции, что совсем другое выражение (которому мы должны примените вышеприведенные правила).

И это действительно так. Все остальное – это lvalue. Мне достаточно легко рассуждать о выражениях таким образом, так как все категории выше легко узнаваемы. Например, легко просмотреть выражение, исключить случаи, описанные выше, и решить, что это значение lvalue. Даже для категории 4, которая имеет более длинное описание, выражения легко узнаваемы (я изо всех сил старался сделать это одним лайнером, но в конечном итоге не удалось).

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


Заметки:

  • Что касается категории 1:
    • this в примере относится к this значению указателя, а не к *this .
    • Строковые литералы являются lvalues, потому что они представляют собой массивы статической продолжительности хранения, поэтому они не вписываются в категорию 1 (они связаны с объектами).
  • Некоторые примеры, относящиеся к категориям 2 и 3:
    • Учитывая объявление int& f(int) , выражение f(7) не генерирует объект по значению, поэтому он не подходит для категории 2; он генерирует ссылку, но это не ссылка rvalue, поэтому категория 3 также не применяется; выражение является значением l.
    • Учитывая объявление int&& f(int) , выражение f(7) генерирует ссылку rvalue; категория 3 применяется здесь, поэтому выражение является значением r.
    • Учитывая объявление int f(int) , выражение f(7) генерирует объект по значению; категория 2 применяется здесь, выражение является значением r.
    • Для бросков мы можем применить те же рассуждения, что и для трех описанных выше маркеров.
    • Учитывая объявление int&& a , использование выражения a не создает ссылку rvalue; он просто использует идентификатор ссылочного типа. Категория 3 не применяется, выражение является lvalue.
    • Лямбда-выражения генерируют объекты замыкания по значению – они относятся к категории 2.
  • Некоторые примеры, относящиеся к категории 4:
    • x->y переводится на (*x).y . *x является lvalue (оно не соответствует ни одной из вышеперечисленных категорий). Итак, если y – нестатический объект -член, x->y является lvalue (он не соответствует категории 4 из-за *x и не подходит в 6, потому что в нем говорится только о функциях-членах).
    • В xy , если y является статическим членом, то категория 4 не применяется. Такое выражение всегда является lvalue, даже если x является rvalue (6 тоже не применяется, потому что он говорит о нестатических функциях-членах).
    • В xy , если y имеет тип T& или T&& , то это не объект- член (помните, объекты , а не ссылки, а не функции), поэтому категория 4 не применяется. Такое выражение всегда является значением lvalue, даже если x является значением r и даже если y является ссылкой на rvalue.
  • Категория 4 в C ++ 11 немного отличалась, но я считаю, что эта формулировка верна для C ++ 14. (Если вы настаиваете на том, чтобы знать, что подписка на атрибут rvalue используется как lvalue в C ++ 11, но является значением x в C ++ 14 – issue 1213.)
  • Дальнейшее разделение r значений на значения x и prvalues ​​относительно просто для C ++ 14: категории 1, 2, 5 и 6 являются prvalues, 3 и 4 – значения x. Для C ++ 11 были немного разные: категория 4 была разделена между значениями, значениями x и lvalues ​​(измененными, как указано выше, а также как часть решения проблемы 616 ). Это может быть важно, поскольку это может повлиять на тип, который вы возвращаете из decltype , например.

Все ссылки на N4140, последний проект C ++ 14 перед публикацией.

Сначала я нашел последние два специальных случая rvalue (все также в стандарте, конечно, но сложнее найти). Обратите внимание, что не все на этой странице является точным для C ++ 14. Он также содержит очень хорошее резюме по обоснованию основных категорий ценности (вверху).

  • Существует ли reference_wrapper для ссылок rvalue?
  • Передача rvalues ​​через std :: bind
  • Почему `std :: move` называется` std :: move`?
  • Преимущества использования передовой
  • Что является прецедентом для перегрузки функций-членов в ссылочных квалификаторах?
  • Есть ли случай, когда полезно использовать ссылку RValue Reference (&&)?
  • В C ++ какие категории (lvalue, rvalue, xvalue и т. Д.) Могут выражать выражения, которые производят временные classы типа classа?
  • Нужны ли ссылки rvalue на const?
  • Переместить захват в lambda
  • Давайте будем гением компьютера.