Почему тернарный оператор с запятыми оценивает только одно выражение в истинном случае?


119

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

Объясните, что делает следующее выражение: someValue ? ++x, ++y : --x, --y

Что мы знаем? Мы знаем, что тернарный оператор имеет более высокий приоритет, чем оператор запятой. С бинарными операторами это было довольно легко понять, но с тернарным оператором я немного борюсь. С бинарными операторами «имеющий более высокий приоритет» означает, что мы можем использовать круглые скобки вокруг выражения с более высоким приоритетом, и это не изменит выполнение.

Для тернарного оператора я бы сделал:

(someValue ? ++x, ++y : --x, --y)

эффективно приводя к тому же коду, который не помогает мне понять, как компилятор сгруппирует код.

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

Затем я выполнил программу двумя способами:

#include <iostream>

int main()
{
    bool someValue = true;
    int x = 10, y = 10;

    someValue ? ++x, ++y : --x, --y;

    std::cout << x << " " << y << std::endl;
    return 0;
}

Результаты в:

11 10

А с другой стороны с someValue = falseнего печатает:

9 9

Почему компилятор C ++ генерирует код, который для истинной ветви тернарного оператора только увеличивает x, а для ложной ветви тернарного оператора уменьшает и xи y?

Я даже зашел так далеко, что заключил настоящую ветку в круглые скобки:

someValue ? (++x, ++y) : --x, --y;

но это все равно приводит к 11 10.


5
«Приоритет» - это новое явление в C ++. Может быть проще просто посмотреть непосредственно на грамматику языка и увидеть, как работают выражения.
Керрек С.Б.

26
Нам не важно , что много о принципах. :-) Тот факт, что вы должны задать этот вопрос здесь, указывает на то, что код никогда не пройдет проверку кода вашими коллегами-программистами. Это делает знание о том , как это на самом деле работает менее полезно. Если , конечно, вы не хотите участвовать в конкурсе Obfuscated C Contest .
Bo Persson

5
@BoPersson без подобных примеров, на которых можно было бы поучиться, будущие обозреватели никогда не узнают, почему они должны отклонять это от производства.
Леушенко

8
@Leushenko - Предупреждающие колокола все равно должны звенеть. Множественные увеличения и уменьшения в одном операторе (ding, ding, ding!). Тернарный оператор, когда вы можете использовать if-else (ding, ding, ding!). Погодите, эти запятые - ужасный оператор запятой? (ding, DING, DING!) Может ли быть какой-то приоритет со всеми этими операторами? (динь, динь, динь!) Так что мы никогда не сможем это использовать. Тогда зачем тратить время на выяснение того, что он делает?
Bo Persson

4
Незначительные нит: имя для ?это условный оператор . Термин тернарный оператор просто означает оператор с тремя операндами. Условный оператор - один из примеров тернарного оператора, но язык может (теоретически) иметь несколько тернарных операторов.
bta

Ответы:


122

Как сказал @Rakete в своем превосходном ответе, это сложно. Я бы хотел добавить к этому немного.

Тернарный оператор должен иметь вид:

? выражение - логическое-выражение- выражение- : присваивание

Итак, у нас есть следующие сопоставления:

  • someValue: логическое-или-выражение
  • ++x, ++y: выражение
  • ??? такое присваивание-выражение --x, --y или только --x?

Фактически это происходит только --xпотому, что выражение присваивания не может быть проанализировано как два выражения, разделенных запятой (в соответствии с правилами грамматики C ++), поэтому --x, --yего нельзя рассматривать как выражение присваивания .

В результате часть тернарного (условного) выражения будет выглядеть так:

someValue?++x,++y:--x

Для удобства чтения ++x,++yможет быть полезно рассматривать вычисление как если бы оно было заключено в круглые скобки (++x,++y); все, что содержится между ?и, :будет упорядочено после условного. (Я заключу их в скобки в оставшейся части сообщения).

и оценивается в таком порядке:

  1. someValue?
  2. (++x,++y)или --x(в зависимости от boolрезультата 1.)

Это выражение затем обрабатывается как левое подвыражение оператора запятой, а правое подвыражение имеет --yследующий вид:

(someValue?(++x,++y):--x), --y;

Это означает, что левая сторона является выражением отброшенного значения , что означает, что она определенно оценивается, но затем мы оцениваем правую часть и возвращаем ее.

Так что же происходит, когда someValueесть true?

  1. (someValue?(++x,++y):--x)выполняет и увеличивает xи yбыть 11и11
  2. Левое выражение отбрасывается (хотя побочные эффекты приращения остаются)
  3. Мы оцениваем правую часть оператора запятой:, --yкоторый затем уменьшается yобратно до10

Чтобы «исправить» поведение, вы можете сгруппировать его в --x, --yкруглые скобки, чтобы преобразовать его в основное выражение, которое является допустимой записью для выражения присваивания *:

someValue?++x,++y:(--x, --y);

* Это довольно забавная длинная цепочка, которая соединяет выражение- присваивание с первичным выражением:

выражение-присваивание --- (может состоять из) -> условное-выражение -> логическое-или-выражение -> логическое-и-выражение -> включающее-или-выражение -> исключающее-или-выражение - -> и-выражение -> выражение-равенство -> выражение-отношения -> выражение-сдвиг -> аддитивное-выражение -> мультипликативное-выражение -> выражение-pm -> выражение-приведение -> унарное-выражение -> постфиксное-выражение -> первичное-выражение


10
Спасибо, что взяли на себя труд разобраться в правилах грамматики; это показывает, что грамматика C ++ - это больше, чем вы найдете в большинстве учебников.
sdenham

4
@sdenham: Когда люди спрашивают, почему «языки, ориентированные на выражения» хороши (т.е. когда { ... }их можно рассматривать как выражение), у меня теперь есть ответ => это для того, чтобы избежать необходимости вводить оператор запятой, который ведет себя таким хитрым образом.
Matthieu M.

Не могли бы вы дать мне ссылку, чтобы прочитать о assignment-expressionцепочке?
MiP

@MiP: Я поднял его из самого стандарта, вы можете найти его под gram.expr
AndyG

88

Вау, это сложно.

Компилятор видит ваше выражение как:

(someValue ? (++x, ++y) : --x), --y;

Тернарному оператору нужен a :, он не может стоять сам по себе в этом контексте, но после него нет причин, по которым запятая должна принадлежать ложному регистру.

Теперь может быть понятнее, почему вы получили такой результат. Если someValueэто правда, то ++x, ++yи --yполучить казнены, который не эффективно изменить , yно прибавляет к x.

Если someValueложно, то --xи --yвыполняются с уменьшением их обоих на единицу.


42

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

Вы неверно истолковали то, что произошло. Истинная ветвь увеличивает как xи y. Однако yсразу после этого уменьшается безоговорочно.

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

   (someValue ? ++x, ++y : --x), (--y);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^

Обратите внимание на «осиротевшие» --yпосле запятой. Это то, что приводит к уменьшению y, которое было изначально увеличено.

Я даже зашел так далеко, что заключил настоящую ветку в круглые скобки:

someValue ? (++x, ++y) : --x, --y;

Вы были на правильном пути, но заключили в скобки неправильную ветвь: вы можете исправить это, заключив в скобки ветку else, например:

someValue ? ++x, ++y : (--x, --y);

Демо (отпечатки 11 11)


5

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

a ? b++, c++ : d++

рассматривается как:

a ? (b++, c++) : d++

(запятая ведет себя так, как будто она имеет более высокий приоритет). С другой стороны,

a ? b++ : c++, d++

рассматривается как:

(a ? b++ : c++), d++

а тернарный оператор имеет более высокий приоритет.


Я думаю, что это все еще в рамках приоритета, поскольку для средней строки существует только один допустимый синтаксический анализ, верно? Тем не менее, полезный пример
sudo rm -rf slash

2

В ответах упускается из виду (хотя и упоминается в комментариях) то, что условный оператор неизменно используется (преднамеренно?) В реальном коде как ярлык для присвоения одного из двух значений переменной.

Итак, более широкий контекст:

whatIreallyWanted = someValue ? ++x, ++y : --x, --y;

Что на первый взгляд абсурдно, поэтому преступления многообразны:

  • Язык допускает нелепые побочные эффекты при выполнении задания.
  • Компилятор не предупреждал вас, что вы делаете странные вещи.
  • Книга, кажется, сосредоточена на «хитрых» вопросах. Можно только надеяться, что ответ на обороте был: «То, что делает это выражение, зависит от странных крайних случаев в надуманном примере, чтобы вызвать побочные эффекты, которых никто не ожидает. Никогда не делайте этого».

1
Назначение одной из двух переменных является обычным случаем тернарного оператора, но бывают случаи, когда полезно иметь форму выражения if(например, выражение приращения в цикле for). Чем больше контекста может также быть for (x = 0, y=0; x+y < 100; someValue?(++x, ++y) :( --x, --y))с петлей , которая может изменить xи yнезависимо друг от друга.
Мартин Боннер поддерживает Монику

@MartinBonner Я не уверен, и этот пример, кажется, очень хорошо отражает точку зрения Боперсона, поскольку он процитировал Тони Хора.
Тарин
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.