Подтвердить что ты не робот

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

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

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

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

Мои вопросы:

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

Ответ 1

Это один из самых распространенных "правил", используемых для объяснения, в чем разница между значениями l и значениями.

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

В начале был C

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

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

На языке, таком как 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

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

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

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

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

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

f() = g();

- это утверждение, которое не имеет смысла в 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 ok

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

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

Что изменилось в С++ 11

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

Ответ 3

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

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

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


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

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

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

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

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

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

Что это.

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

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

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

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