В C ++ почему true && true || false && false == правда? - PullRequest
16 голосов
/ 31 октября 2010

Я хотел бы знать, если кто-то знает, как компилятор интерпретирует следующий код:

#include <iostream>
using namespace std;

int main() {
 cout << (true && true || false && false) << endl; // true
}

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

Ответы [ 12 ]

34 голосов
/ 31 октября 2010

&& имеет более высокий приоритет, чем ||.

29 голосов
/ 01 ноября 2010

Caladain имеет абсолютно правильный ответ, но я хотел ответить на один из ваших комментариев на его ответ:

При коротком замыкании || происходит оператор и замыкает выполнение второго выражения &&, что означает || оператор был выполнен ДО второго оператора &&. Это подразумевает выполнение слева направо для && и || (не && приоритет).

Я думаю, что часть проблемы, с которой вы столкнулись, заключается в том, что приоритет не совсем означает, что вы думаете, что он означает. Это правда, что && имеет более высокий приоритет, чем ||, и это точно объясняет поведение, которое вы видите. Рассмотрим случай с обычными арифметическими операторами: предположим, что у нас есть a * b + c * (d + e). Что говорит нам приоритет, это как вставить круглые скобки: сначала около *, затем около +. Это дает нам (a * b) + (c * (d + e)); в вашем случае у нас есть (1 && 1) || (infiniteLoop() && infiniteLoop()). Затем представьте, что выражения становятся деревья . Для этого преобразуйте каждый оператор в узел с двумя аргументами в качестве дочерних:

Expression trees.

При оценке этого дерева возникает короткое замыкание. В арифметическом дереве вы можете представить себе стиль исполнения сверху вниз: сначала оцените DE = d + e, затем AB = a * b и CDE = c * DE и окончательный вариант результат AB + CDE. Но учтите, что вы могли бы в равной степени хорошо оценить сначала AB, затем DE, CDE и окончательный результат; ты не можешь отличить Однако, поскольку || и && имеют короткое замыкание, они имеют , чтобы использовать эту самую левую первую оценку. Таким образом, чтобы оценить ||, мы сначала оцениваем 1 && 1. Так как это правда, || замыкает накоротко и игнорирует его правую ветвь - даже если , если бы он это оценил, ему пришлось бы сначала оценить infiniteLoop() && infiniteLoop().

Если это поможет, вы можете думать о каждом узле в дереве как о вызове функции, который выдает следующее представление plus(times(a,b), times(c,plus(d,e))) в первом случае и or(and(1,1), and(infiniteLoop(),infiniteLoop()) во втором случае. Короткое замыкание означает, что вы должны полностью оценить каждый левый аргумент функции в or или and; если это true (для or) или false (для and), тогда игнорируйте правый аргумент.

Ваш комментарий предполагает, что мы сначала оцениваем все с наивысшим приоритетом, затем все с наивысшим приоритетом и т. Д. И т. Д., Что соответствует выполнению дерева снизу вверх в порядке снизу вверх. Вместо этого происходит то, что этот приоритет говорит нам, как построить дерево. Правила выполнения дерева не имеют значения в простом арифметическом случае; однако короткое замыкание является точной спецификацией того, как оценивать дерево.


Редактировать 1: В одном из ваших других комментариев вы сказали

Ваш арифметический пример требует, чтобы два умножения были оценены перед окончательным сложением, разве это не то, что определяет приоритет?

Да, именно это определяет приоритет, за исключением того, что это не совсем так. Это, безусловно, верно для C , но подумайте, как бы вы оценили (не-C!) Выражение 0 * 5 ^ 7 в своей голове, где 5 ^ 7 = 5<sup>7</sup> и ^ имеют более высокий приоритет, чем *. Согласно вашему правилу «снизу вверх», нам нужно оценить 0 и 5 ^ 7, прежде чем мы сможем найти результат. Но вы не потрудитесь оценить 5 ^ 7; Вы бы просто сказали «ну, так как 0 * x = 0 для всех x, это должно быть 0», и пропустите всю правую ветвь. Другими словами, я не оценил обе стороны полностью, прежде чем оценивать окончательное умножение; Я замкнул. Точно так же, поскольку false && _ == false и true || _ == true для любого _, нам может не потребоваться касаться правой стороны; это то, что для оператора означает короткое замыкание. C не умножает короткое замыкание (хотя язык мог сделать это), но оно делает короткое замыкание && и ||.

Так же, как короткое замыкание 0 * 5 ^ 7 не меняет обычных правил приоритета PEMDAS, короткое замыкание логических операторов не меняет того факта, что && имеет более высокий приоритет, чем ||. Это просто ярлык. Так как мы должны сначала выбрать некоторую сторону оператора, C обещает сначала вычислить левую часть логических операторов; как только это будет сделано, есть очевидный (и полезный) способ избежать оценки правой части для определенных значений, и Си обещает сделать это.

Ваше правило - оценивать выражение сверху вниз - также хорошо определено, и язык может сделать это. Однако его недостатком является то, что он не допускает короткого замыкания, что является полезным поведением. Но если каждое выражение в вашем дереве четко определено (без циклов) и чисто (без изменяющихся переменных, печати и т. Д.), Тогда вы не сможете увидеть разницу. Только в этих странных случаях, которые математические определения «и» и «или» не охватывают, такое короткое замыкание даже заметно.

Также обратите внимание, что нет ничего фундаментального в том факте, что короткое замыкание работает, отдавая приоритет левому выражению. Можно определить язык Ɔ, где ⅋⅋ представляет and, а \\ представляет ||, но где 0 ⅋⅋ infiniteLoop() и 1 \\ infiniteLoop() будут зацикливаться, а infiniteLoop() ⅋⅋ 0 и infiniteLoop() \\ 1 будут ложными и истинными, соответственно. Это просто соответствует выбору сначала оценить правую часть вместо левой, а затем упростить таким же образом.

В двух словах: что говорит нам приоритет, это как построить дерево разбора. Единственные разумные порядки для оценки дерева разбора - это те, которые ведут себя , как если бы мы оценивали его снизу вверх (как вы хотите) для четко определенных чистых значений . Для неопределенных или нечистых значений необходимо выбрать некоторый линейный порядок. 1 После выбора линейного порядка определенные значения для одной стороны оператора могут однозначно определять результат всего выражения ( например , 0 * _ == _ * 0 == 0, false && _ == _ && false == false или true || _ == _ || true == true). Из-за этого вы можете уйти, не завершив оценку того, что последует в линейном порядке; C обещает сделать это для логических операторов && и ||, оценивая их слева направо, и не делать этого ни для чего другого. Однако благодаря старшинству мы делаем знаем, что true || true && false - это true, а не false: потому что

  true || true && false
→ true || (true && false)
→ true || false
→ true

вместо

  true || true && false
↛ (true || true) && false
→ true && false
→ false

1: На самом деле, мы также могли бы теоретически оценить обе стороны оператора параллельно, но это сейчас не важно, и, конечно, не имеет смысла для C. Это дает более гибкую семантику , но тот, который имеет проблемы с побочными эффектами (когда они случаются?).

22 голосов
/ 31 октября 2010

(true && true || false && false) оценивается как && с более высоким приоритетом.

TRUE && TRUE = True

FALSE && FALSE = False

True || False = True

Обновление:

1&&1||infiniteLoop()&&infiniteLoop()

Почему это дает истину в C ++?

Как и прежде, давайте разберем его на части. && имеет более высокий приоритет, чем || и логические операторы короткого замыкания в C ++.

1 && 1 = True.

Когда значение bool конвертируется в целочисленное значение, тогда

false -> 0
true -> 1

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

В среде без короткого замыкания это выражение будет зависать вечно, потому что обе стороны ИЛИ будут "оценены", а правая сторона зависнет.

Если вы не уверены в приоритете, вот как все будет оцениваться в вашем исходном сообщении, если || имеет более высокий приоритет, чем &&:

1st.) True || False = True
2nd.) True && 1st = True
3rd.) 2nd && false = false
Expression = False;

Я не могу вспомнить, идет ли он справа налево или слева направо, но в любом случае результат будет одинаковым. В вашем втором посте, если || имел более высокий приоритет:

1st.) 1||InfLoop();  Hang forever, but assuming it didn't
2nd.) 1 && 1st;
3rd.) 2nd && InfLoop(); Hang Forever

tl; dr: Первоочередной задачей является оценка && в первую очередь, но компилятор также замыкает ИЛИ. По сути, компилятор группирует порядок операций, подобных этому (SIMPLISTIC VIEW, положите вниз вилы :-P)

1st.) Is 1&&1 True?
2nd.) Evaluate if the Left side of the operation is true, 
      if so, skip the second test and return True,
      Otherwise return the value of the second test(this is the OR)
3rd.) Is Inf() && Inf() True? (this would hang forever since 
      you have an infinite loop)

Обновление № 2: «Однако этот пример доказывает, что && НЕ имеет приоритета, так как || оценивается перед вторым &&. Это показывает, что && и || имеют одинаковый приоритет и оцениваются в порядке слева направо."

"Если бы && имел приоритет, он вычислил бы первый && (1), затем второй && (бесконечные циклы) и повесил программу. Поскольку этого не происходит, && не оценивается до ||."

Давайте рассмотрим это подробно.

Мы говорим здесь о двух разных вещах. Приоритет, который определяет порядок операций, и короткое замыкание, которое является трюком компилятора / языка для сохранения циклов процессора.

Давайте сначала рассмотрим приоритет. Приоритет - это сокращение от «Порядок операций». По сути, это утверждение: 1 + 2 * 3 в каком порядке должны быть сгруппированы операции для оценки?

Математика четко определяет порядок операций, давая умножение более высокий приоритет, чем сложение.

1 + (2 * 3) = 1 + 2 * 3
2 * 3 is evaluated first, and then 1 is added to the result.
* has higher precedence than +, thus that operation is evaluated first.

Теперь давайте перейдем к логическим выражениям: (&& = AND, || = OR)

true AND false OR true

C ++ дает AND более высокий приоритет, чем OR, таким образом

(true AND false) OR true
true AND false is evaluated first, and then 
      used as the left hand for the OR statement

Таким образом, только по приоритету (true && true || false && false) будет работать в следующем порядке:

((true && true) || (false && false)) = (true && true || false && false)
1st Comparison.) true && true
2nd Comparison.) false && false
3rd Comparison.) Result of 1st comparison || Result of Second

Со мной до сих пор? Теперь перейдем к короткому замыканию: В C ++ логические операторы называются «короткозамкнутыми». Это означает, что компилятор будет смотреть на данный оператор и выбирать «лучший путь» для оценки. Возьмите этот пример:

(true && true) || (false && false)
There is no need to evaluate the (false && false) if (true && true) 
equals true, since only one side of the OR statement needs to be true.
Thus, the compiler will Short Circuit the expression.  Here's the compiler's
Simplified logic:
1st.) Is (true && true) True?
2nd.) Evaluate if the Left side of the operation is true, 
      if so, skip the second test and return True,
      Otherwise return the value of the second test(this is the OR)
3rd.) Is (false && false) True? Return this value

Как вы можете видеть, если (true && true) оценивается как TRUE, то нет необходимости тратить такты на оценку, если (false && false) имеет значение true.

C ++ Всегда короткие замыкания, но другие языки предоставляют механизмы для так называемых «нетерпеливых» операторов.

Возьмем, к примеру, язык программирования Ada. В Аде «И» и «ИЛИ» являются «нетерпеливыми» операторами… они заставляют все оценивать.

В Ada (истина и истина) ИЛИ (ложь И ложь) будет оценивать как (истина И истина), так и (ложь И ложь) перед оценкой ИЛИ. Ада также дает вам возможность короткого замыкания с И, ТО, ИЛИ ИЛИ, что даст вам то же поведение, что и С ++.

Надеюсь, это полностью ответит на ваш вопрос. Если нет, дайте мне знать: -)

Обновление 3: Последнее обновление, а затем я продолжу по электронной почте, если у вас все еще есть проблемы.

"Если происходит короткое замыкание оператора || и короткое замыкание при выполнении второго выражения &&, это означает, что оператор || был выполнен ДО второго оператора &&. Это подразумевает выполнение слева направо для && и|| (не && приоритет). "

Давайте посмотрим на этот пример:

(false && infLoop()) || (true && true) = true (Put a breakpoint in InfLoop and it won't get hit)
false && infLoop() || true && true = true  (Put a breakpoint in InfLoop and it won't get hit)
false || (false && true && infLoop()) || true = false (infLoop doesn't get hit)

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

Теперь давайте посмотрим на это:

(false || true && infLoop() || true);

Infloop вызывается!Если OR имеет более высокий приоритет, чем &&, тогда компилятор оценит:

(false || true) && (infLoop() || true) = true;
(false || true) =true
(infLoop() || true = true (infLoop isn't called)

Но вызывается InfLoop!Вот почему:

(false || true && infLoop() || true);
1st Comparison.) true && InfLoop() (InfLoop gets called)
2nd Comparison.) False || 1st Comp (will never get here)
3rd Comparison.) 2nd Comp || true; (will never get here)

Precendece ONLY устанавливает группировку операций.При этом && больше, чем ||.

true && false || true && true gets grouped as
(true && false) || (true && true);

Компилятор Затем появляется и определяет, в каком порядке он должен выполнить оценку, чтобы дать ему наилучшую возможность для сохранения циклов.

Consider: false && infLoop() || true && true
Precedence Grouping goes like this:
(false && infLoop()) || (true && true)
The compiler then looks at it, and decides it will order the execution in this order:
(true && true) THEN || THEN (false && InfLoop())

Это своего рода факт ... и я не знаю, как еще это продемонстрировать.Приоритет определяется правилами грамматики языка.Оптимизация компилятора определяется каждым компилятором. Некоторые лучше, чем другие, но Все могут свободно переупорядочивать сгруппированные сравнения по своему усмотрению, чтобы дать ему «лучший» шанс для быстрого выполнения снаименьшее количество сравнений.

7 голосов
/ 01 ноября 2010

Два факта объясняют поведение обоих примеров. Во-первых, приоритет && выше, чем ||. Во-вторых, оба логических оператора используют оценку короткого замыкания.

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

Логические операторы имеют специальное свойство: в некоторых случаях, если одна сторона оценивает конкретное значение, тогда значение оператора известно независимо от значения на другой стороне. Чтобы сделать это свойство полезным, язык C (и, соответственно, каждый C-подобный язык) определил логические операторы для оценки LHS перед RHS и, кроме того, для оценки RHS, только если его значение равно требуется знать результат оператора.

Итак, предполагая, что обычные определения TRUE и FALSE, TRUE && TRUE || FALSE && FALSE оценивается, начиная слева. Первый TRUE не приводит к принудительному результату первого &&, поэтому вычисляется второй TRUE, а затем вычисляется выражение TRUE && TRUE (TRUE). Теперь || знает свою LHS. Более того, его LHS заставил узнать результат ||, поэтому он пропускает оценку всей RHS.

Точно такой же порядок оценки применяется во втором случае. Поскольку RHS || не имеет значения, он не оценивается, и не выполняется ни вызов infiniteLoop().

Такое поведение разработано и полезно. Например, вы можете написать p && p->next, зная, что выражение никогда не будет пытаться разыменовать нулевой указатель.

7 голосов
/ 31 октября 2010

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

3 голосов
/ 07 декабря 2012

"Если происходит короткое замыкание оператора || и короткое замыкание при выполнении второго выражения &&, это означает, что оператор || был выполнен ДО второго оператора &&. Это подразумевает выполнение слева направодля && и || (не && приоритет). "

Не совсем.

(xx && yy || zz && qq)

Будет оцениваться так:

  1. Сначала проверьтеoperator.
  2. Оценить xx && yy
  3. Проверить следующий оператор.
  4. Если следующий оператор - ||, а первая часть оператора - true, пропустить остальные.
  5. В противном случае проверьте следующий оператор после || и оцените его: zz && qq
  6. Наконец, оцените ||.

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

1 голос
/ 04 ноября 2010

Так как и / или / true / false очень похожи на * / + / 1/0 (они математически эквивалентны), верно также следующее:

1 * 1 + 0 * 0 == 1

и довольно легкопомните ...

Так что да, это связано с приоритетом и короткого замыкания.Приоритет булевых операций довольно прост, если вы сопоставите его с соответствующими им целочисленными операциями.

1 голос
/ 01 ноября 2010

Относительно последнего кода Эндрю,

#include <iostream>
using namespace std;

bool infiniteLoop () {
    while (true);
    return false;
}

int main() {
    cout << (true && true || infiniteLoop() && infiniteLoop()) << endl; // true
}

Оценка короткого замыкания означает, что вызовы infiniteLoop гарантированно не будут выполнены.

Однако, это интересно извращенным способом, потому что черновик C ++ 0x делает бесконечный цикл, который ничего не делает Неопределенное поведение . Это новое правило, как правило, считается очень нежелательным и глупым, даже прямо опасным, но оно как бы проникло в проект. Частично из соображений сценариев многопоточности, где автор одной статьи думал, что это упростит правила для чего-то или иного, довольно не относящегося к делу.

Таким образом, с компилятором, который находится на «переднем крае» C ++ 0x-соответствия, программа может завершиться с некоторым результатом, даже если она выполнила вызов infiniteLoop! Конечно, с таким компилятором он мог бы также создать это страшное явление, носовые демоны ...

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

Приветствия & hth.,

1 голос
/ 31 октября 2010

в примере true && true || infiniteLoop() && infiniteLoop() ни один из вызовов бесконечного цикла не оценивается из-за двух объединенных характеристик: && имеет приоритет над ||, и ||короткое замыкание, когда левая сторона верна.

если && и ||имеет тот же приоритет, оценка должна идти так:

((( true && true ) || infiniteLoop ) && infiniteLoop )
(( true || infiniteLoop ) && infiniteLoop )
=> first call to infiniteLoop is short-circuited
(true && infiniteLoop) => second call to infiniteLoop would have to be evaluated

, но из-за приоритета && оценка на самом деле идет

(( true && true ) || ( infiniteLoop && infiniteLoop ))
( true || ( infiniteLoop && infiniteLoop ))
=> the entire ( infiniteLoop && infiniteLoop ) expression is short circuited
( true )
1 голос
/ 31 октября 2010

относительно вашего редактирования: infiniteLoop () не будет оцениваться, потому что true || (что угодно) всегда верно. Используйте истину | (что угодно), если что должно быть выполнено.

...