Почему стандартная грамматика C ++ для выражения присваивания выглядит так странно

Из стандарта C ++ грамматика для выражения присваивания выглядит следующим образом:

assignment-expression:
conditional-expression
logical-or-expression assignment-operator assignment-expression
throw-expression

assignment-operator: one of
= *= /= %= += -= >>= <<= &= ^= |=

Обратите внимание, что левая часть «оператора присваивания» является «логическим выражением или выражением», то есть что-то вроде (4 || localvar1) = 5; является допустимым выражением присваивания в соответствии с грамматикой. Это не имеет смысла для меня. Почему они выбирают «логическое выражение или выражение» вместо, скажем, идентификатора или id_expression?

1

Решение

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

Причина, как указано выше, заключается в том, что присвоение может применяться к любому именующий выражение перечисления или фундаментального типа или Rvalue выражение типа класса (где operator= всегда член). Многие выражения на языке, допускающем перегрузку операторов и не определяющем тип возвращаемого оператором типа, могут потенциально удовлетворять требованиям присваивания, и грамматика должна разрешать все эти варианты использования.

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

2

Другие решения

Ваше конкретное заявление, (4 || localvar1) = 5; является недействительным (если operator|| перегружен), потому что вы не можете назначить 5 на 4 (4 является г-значение). Вы должны иметь л-значение (что-то, что может быть назначено) слева, например, ссылка, возвращаемая функцией.

Например, скажем, у вас есть какая-то функция int& get_my_int() который возвращает ссылку на целое число. Затем вы можете сделать это:

`get_my_int() = 5;`

Это установит целое число, возвращаемое get_my_int() в 5,
Как и в вашем первом посте, это ДОЛЖНО быть ссылкой на целое число (а не на значение); в противном случае вышеприведенное утверждение не скомпилируется.

2

На самом деле в грамматике C ++ для операторов присваивания есть две интересные вещи, ни одна из которых не связана с действительностью:

 (4 || localvar1) = 5;

Это выражение синтаксически допустимо (вплоть до проверки типов) из-за круглых скобок. любой Заключенное в скобки выражение ссылочного типа является синтаксически правильным в левой части оператора присваивания. (И, как было отмечено, почти любое выражение, которое включает пользовательский тип или функцию, может иметь ссылочный тип в результате перегрузки оператора.)

Что более интересно в грамматике, так это то, что она устанавливает левый приоритет операторов присваивания как ниже, чем почти все другие операторы, включая логические или, так что вышеприведенное выражение семантически эквивалентно

4 || localvar1 = 5;

хотя многие читатели интерпретируют вышесказанное как 4 || (localvar1 = 5) (что было бы совершенно правильно, если бы localvar1 имеет тип, который может быть назначен intхотя указанное назначение никогда не произойдет — если, конечно, || перегружен в этом контексте).

Так что же имеет более низкий приоритет в левой части оператора присваивания? Как я уже сказал, очень мало, но одно важное исключение ?::

// Replace the larger of a and b with c
a > b ? a = c : b = c;

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

// Replace c with the larger of a and b
c = a > b ? a : b;

Единственные другие операторы, которые связываются менее плотно слева от оператора присваивания, чем оператор присваивания, являются , оператор и другой оператор присваивания. (Другими словами, назначение правоассоциативной в отличие от почти всех других бинарных операторов.) Ни один из них не удивителен — на самом деле, они настолько необходимы, что легко упустить, насколько важно разработать грамматику таким образом. Считайте следующее непримечательным for пункт:

for (first = p = vec.begin(), last = vec.end(); p < last; ++p)

Здесь , является оператором запятой, и он явно должен связываться менее плотно, чем любое из назначений, которые его окружают. (C и C ++ являются исключительными только в этом синтаксисе, имея оператор запятой; в большинстве языков , не считается оператором.) Кроме того, было бы нежелательно, чтобы первое выражение присваивания было проанализировано как (first = p) = vec.begin(),

Тот факт, что операторы присваивания связываются справа, ничем не примечательн, но стоит отметить один исторический интерес. Когда Бьярн Страуструп искал операторов для перегрузки потоков ввода / вывода, он остановился на << а также >> потому что, хотя оператор присваивания мог бы быть более естественным [1], присваивание связывается справа, а потоковый оператор должен связываться слева (std::cout << a << b должно быть (std::cout << a) << b), Тем не менее, так как << связывает гораздо более тесно, чем присваивание, есть несколько ошибок при использовании потоковых операторов. (Последнее, что меня поймало, это то, что сдвиг связывает более тесно, чем побитовые операторы.)

[Примечание 1]: у меня нет на это цитаты, но я помню, что читал это много лет назад в Язык программирования C ++. Насколько я помню, не было единого мнения о естественности операторов присваивания, но кажется более естественным, чем перегрузка операторов сдвига, быть чем-то совершенно отличным от их нормальной семантики.

1
По вопросам рекламы [email protected]