Обязательны ли закорачивающие логические операторы? А порядок оценки?

143

Есть ли стандарт ANSI мандат логических операторов закоротить, в С или C ++?

Я смущен, потому что я помню книгу K&R, в которой говорилось, что ваш код не должен зависеть от короткого замыкания этих операций, поскольку они могут и не быть. Может ли кто-нибудь указать, где в стандарте сказано, что логические операции всегда закорачиваются? Меня больше всего интересует C ++, было бы здорово ответить и на C.

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

Указывает ли стандарт порядок оценки этого выражения?

if( functionA() && functionB() && functionC() ) cout<<"Hello world";
Джо Пинеда
источник
13
Осторожно: это верно для типов POD. Но если вы перегрузите оператор && или оператор || для определенного класса это НЕ Я повторяю, НЕ ярлык. Вот почему рекомендуется НЕ определять эти операторы для ваших собственных классов.
Мартин Йорк,
Я переопределил эти операторы некоторое время назад, когда создавал класс, который будет выполнять некоторые базовые операции логической алгебры. Наверное, стоит наклеить предупреждающий комментарий "это уничтожает короткое замыкание и оценку слева направо!" на случай, если я забуду это. Также перегрузил * / + и сделал их синонимами :-)
Джо Пинеда
Наличие вызовов функций в блоке if не является хорошей практикой программирования. Всегда объявляйте переменную, которая содержит возвращаемое значение метода, и используйте ее в блоке if.
SR Chaitanya
6
@SRChaitanya Это неверно. То, что вы произвольно называете плохой практикой, делается постоянно, особенно с функциями, возвращающими логические значения, как здесь.
Marquis of Lorne

Ответы:

158

Да, короткие замыкания и порядок оценки требуются для операторов ||и &&в стандартах C и C ++.

Стандарт C ++ говорит (в стандарте C должно быть эквивалентное предложение):

1.9.18

При оценке следующих выражений

a && b
a || b
a ? b : c
a , b

используя встроенное значение операторов в этих выражениях, после вычисления первого выражения (12) есть точка последовательности .

В C ++ есть дополнительная ловушка: короткое замыкание НЕ применяется к типам, которые перегружают операторы ||и &&.

Сноска 12: Операторы, указанные в этом параграфе, являются встроенными операторами, как описано в разделе 5. Когда один из этих операторов перегружен (раздел 13) в допустимом контексте, тем самым определяя пользовательскую операторную функцию, выражение обозначает вызов функции, а операнды образуют список аргументов без подразумеваемой точки последовательности между ними.

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

Алекс Б
источник
4
Не знал, что короткое замыкание не применимо к перегруженным логическим операциям, это лишнее. Не могли бы вы добавить ссылку на стандарт или источник? Я не доверяю вам, просто хочу узнать об этом побольше.
Джо Пинеда,
5
да, это логично. он действует как аргумент для оператора && (a, b). именно реализация этого говорит о том, что происходит.
Йоханнес Шауб - лит
12
litb: просто невозможно передать b оператору && (a, b) без его оценки. И нет способа отменить вычисление b, потому что компилятор не может гарантировать отсутствие побочных эффектов.
jmucchiello
3
Мне это грустно. Я бы так подумал, если бы переопределил операторы && и || и они по-прежнему полностью детерминированы , компилятор обнаружит это и сделает их оценку короткой: в конце концов, порядок не имеет значения, и они не гарантируют никаких побочных эффектов!
Джо Пинеда,
4
@Joe: но возвращаемое значение и аргументы оператора могут измениться с логического на другое. Раньше я реализовывал «особую» логику с ТРИ значениями («истина», «ложь» и «неизвестно»). Возвращаемое значение детерминировано, но поведение при коротком замыкании не подходит.
Alex B
71

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

Если бы это было не так, такой код не был бы распространенной идиомой

   char* pChar = 0;
   // some actions which may or may not set pChar to something
   if ((pChar != 0) && (*pChar != '\0')) {
      // do something useful

   }

Раздел 6.5.13 Оператор логического И в спецификации C99 (ссылка в PDF) говорит:

(4). В отличие от побитового двоичного оператора &, оператор && гарантирует оценку слева направо; после вычисления первого операнда стоит точка последовательности. Если первый операнд сравнивается с 0, второй операнд не оценивается.

Точно так же в разделе 6.5.14 Оператор логического ИЛИ говорит

(4) В отличие от побитового | оператор || оператор гарантирует оценку слева направо; после вычисления первого операнда стоит точка последовательности. Если первый операнд не равен 0, второй операнд не оценивается.

Подобную формулировку можно найти в стандартах C ++, см. Раздел 5.14 в этом черновике . Как отмечает проверяющий в другом ответе, если вы переопределите && или ||, тогда оба операнда должны быть оценены, поскольку это становится обычным вызовом функции.

Пол Диксон
источник
Ах, что я искал! Хорошо, так что и порядок оценки, и короткое замыкание требуются согласно ANSI-C 99! Мне бы очень хотелось увидеть эквивалентную ссылку для ANSI-C ++, хотя я почти на 99%, это должно быть то же самое.
Джо Пинеда,
Трудно найти хорошую бесплатную ссылку на стандарты C ++, я связался с черновиком, который я нашел с помощью некоторых поисковых запросов.
Пол Диксон,
Верно для типов POD. Но если вы перегрузите оператор && или оператор || это не ярлык.
Мартин Йорк,
1
да, интересно отметить, что для bool у вас всегда будет гарантированный порядок оценки и поведение при коротком замыкании. потому что вы не можете перегрузить оператор && для двух встроенных типов. вам нужен хотя бы один определяемый пользователем тип в операндах, чтобы он вел себя иначе.
Йоханнес Шауб - лит
Хотел бы я принять и шашки, и этот ответ. Поскольку меня больше всего интересует C ++, я принимаю другой вариант, хотя должен признать, что он тоже великолепен! Большое спасибо!
Джо Пинеда,
19

Да, это требует (и порядок оценки, и короткое замыкание). В вашем примере, если все функции возвращают истину, порядок вызовов строго от functionA, затем functionB и затем functionC. Используется для этого как

if(ptr && ptr->value) { 
    ...
}

То же самое для оператора запятой:

// calls a, then b and evaluates to the value returned by b
// which is used to initialize c
int c = (a(), b()); 

Один говорит между левым и правым операндом &&, ||, ,и между первым и вторым / третьим операндом ?:(условного оператором) является «точкой последовательности». Любые побочные эффекты полностью оцениваются до этого момента. Итак, это безопасно:

int a = 0;
int b = (a++, a); // b initialized with 1, and a is 1

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

// order of calls to a and b is unspecified!
function(a(), b());

Стандарт C ++ гласит 5.14/1:

Группы операторов && расположены слева направо. Оба операнда неявно преобразуются в тип bool (пункт 4). Результат истинен, если оба операнда истинны, и ложь в противном случае. В отличие от &, && гарантирует оценку слева направо: второй операнд не оценивается, если первый операнд ложен.

И в 5.15/1:

|| группы операторов слева направо. Оба операнда неявно преобразуются в bool (раздел 4). Он возвращает истину, если один из его операндов истинен, и ложь в противном случае. В отличие от |, || гарантирует оценку слева направо; более того, второй операнд не оценивается, если первый операнд имеет значение true.

Он говорит для обоих рядом с этими:

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

В дополнение к этому, 1.9/18говорит

При оценке каждого из выражений

  • a && b
  • a || b
  • a ? b : C
  • a , b

используя встроенное значение операторов в этих выражениях (5.14, 5.15, 5.16, 5.18), после вычисления первого выражения есть точка последовательности.

Йоханнес Шауб - litb
источник
9

Прямо из старого доброго K&R:

C гарантирует, что &&и ||оцениваются слева направо - скоро мы увидим случаи, когда это имеет значение.

Джон Т
источник
3
K&R 2-е издание с. 40. «Выражения, связанные с помощью && или ||, оцениваются слева направо, и оценка прекращается, как только становится известна истинность или ложность результата. Большинство программ C полагаются на эти свойства». Я не могу найти цитируемый вами текст в книге. Это из крайне устаревшего 1-го издания? Уточните, пожалуйста, где вы нашли этот текст.
Lundin
2
Хорошо, оказывается, вы цитируете этот древний учебник . Он датирован 1974 годом и не имеет большого значения.
Lundin
4

Будьте очень осторожны.

Для основных типов это операторы быстрого доступа.

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

Для operator &&и operator ||для основных типов порядок оценки слева направо ( в противном случае короткой резки будет трудно :-) Но для перегруженных операторов , которые определяют, в основном это синтаксический сахар для определения метода и , таким образом , порядок оценки параметров неопределенный.

Мартин Йорк
источник
1
Перегрузка оператора не имеет ничего общего с типом POD или нет. Чтобы определить операторную функцию, по крайней мере, один из аргументов должен быть классом (или структурой, или объединением), или перечислением, или ссылкой на один из них. Быть POD означает, что вы можете использовать на нем memcpy.
Дерек Ледбеттер,
Я об этом и говорил. Если вы перегружаете && для своего класса, тогда это действительно просто вызов метода. Таким образом, нельзя полагаться на порядок оценки параметров. Очевидно, вы не можете перегружать && для типов POD.
Мартин Йорк,
3
Вы неправильно используете термин «типы POD». Вы можете перегрузить && для любой структуры, класса, объединения или перечисления, POD или нет. Вы не можете перегрузить &&, если обе стороны являются числовыми типами или указателями.
Дерек Ледбеттер,
Я использовал POD как (char / int / float и т. Д.), А не как агрегирующий POD (о чем вы говорите) и обычно ссылаюсь на него отдельно или более явно, потому что это не встроенный тип.
Мартин Йорк,
2
Значит, вы имели в виду «основные типы», но написали «типы POD»?
Öö Tiib
0

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

Для приоритета, когда у вас есть что-то вроде A op1 B op2 C, вы можете сгруппировать вещи как (A op1 B) op2 Cили A op1 (B op2 C). Если op1имеет более высокий приоритет, чем op2, вы получите первое выражение. В противном случае вы получите второй.

Для ассоциативности, когда у вас есть что-то вроде A op B op C, вы можете снова сгруппировать тонкие как (A op B) op Cили A op (B op C). Если opоставил ассоциативность, мы получаем первое выражение. Если он имеет правильную ассоциативность, мы получаем второй. Это также работает для операторов с таким же уровнем приоритета.

В этом конкретном случае &&имеет более высокий приоритет, чем ||, поэтому выражение будет оцениваться как (a != "" && it == seqMap.end()) || isEven.

Сам порядок в форме дерева выражений - "слева направо". Итак, сначала оценим a != "" && it == seqMap.end(). Если это правда, все выражение истинно, в противном случае мы переходим к isEven. Конечно, процедура рекурсивно повторяется внутри левого подвыражения.


Интересные лакомые кусочки, но концепция приоритета уходит корнями в математическую нотацию. То же самое происходит в a*b + c, где *имеет более высокий приоритет, чем +.

Еще более интересно / непонятно, что для выражения без родительской точки A1 op1 A2 op2 ... opn-1 An, где все операторы имеют одинаковый приоритет, количество деревьев двоичных выражений, которые мы могли бы сформировать, задается так называемыми каталонскими числами . У крупных nони растут очень быстро. d

Хория Коман
источник
Все это правильно, но речь идет о приоритете операторов и ассоциативности, а не о порядке вычисления и кратковременном обращении. Это разные вещи.
Thomas Padron-McCarthy
0

Если вы доверяете Википедии:

[ &&и ||] семантически отличаются от побитовых операторов & и | потому что они никогда не будут оценивать правый операнд, если результат может быть определен только по левому

C (язык программирования)

Софи Альперт
источник
11
Зачем доверять вики, когда у нас есть стандарт!
Мартин Йорк,
1
Если вы доверяете Википедии, «Википедия не является надежным ресурсом» .
Marquis of Lorne
Это верно, но неполно, поскольку перегруженные операторы в C ++ не замыкаются.
Thomas Padron-McCarthy