Почему логический оператор NOT в языках стиля C «!», А не «~~»?

40

Для бинарных операторов у нас есть как побитовые, так и логические операторы:

& bitwise AND
| bitwise OR

&& logical AND
|| logical OR

NOT (унарный оператор) ведет себя по-другому, хотя. Существует ~ для побитового и! для логики.

Я признаю, что NOT - это унарная операция, в отличие от AND и OR, но я не могу придумать причину, по которой дизайнеры решили отклониться от принципа, что single - побитовое, а double - логично, и вместо этого выбрали другой символ. Я думаю, вы могли бы прочитать это неправильно, как двойная побитовая операция, которая всегда возвращала бы значение операнда. Но это не кажется мне реальной проблемой.

Есть ли причина, по которой я скучаю?

Мартин Маат
источник
7
Потому что, если !! логично, нет, как бы я превратил 42 в 1? :)
candied_orange
9
Не было бы ~~тогда более логичным для логического НЕ, если бы вы следовали шаблону, согласно которому логический оператор является удвоением побитового оператора?
Барт ван Инген Шенау
9
Во-первых, если бы это было для последовательности, это было бы ~ и ~~ Удвоение и или или связано с коротким замыканием; и логическое не имеет короткого замыкания.
Кристоф
3
Я подозреваю, что основной причиной дизайна является визуальная ясность и различие в типичных случаях использования. Бинарные (то есть с двумя операндами) операторы являются инфиксными (и имеют тенденцию быть разделенными пробелами), тогда как унарные операторы являются префиксными (и, как правило, не разделяются).
Стив
7
Как уже упоминалось в некоторых комментариях (и для тех, кто не хочет переходить по этой ссылке , !!fooэто весьма распространенная (не распространенная?) Идиома. Она нормализует аргумент «ноль или ненулевое значение» для 0или 1.
Кит Томпсон

Ответы:

110

Странно, но история языка программирования в стиле C начинается не с C.

Деннис Ричи хорошо объясняет проблемы рождения Си в этой статье .

При чтении становится очевидным, что C унаследовал часть своего языкового дизайна от своего предшественника BCPL , и особенно от операторов. Раздел «Неонатальный С» вышеупомянутой статьи объясняет, как BCPL &и |были обогащены двумя новыми операторами &&и ||. Причины были:

  • другой приоритет требовался из-за его использования в сочетании с ==
  • другая логика оценки: слева-правой оценка с коротким замыканием (т.е. , когда aнаходится falseв a&&b, bне оценивается).

Интересно, что это дублирование не создает никакой двусмысленности для читателя: a && bне будет неверно истолковано как a(&(&b)). С точки зрения синтаксического анализа также нет никакой двусмысленности: это &bмогло бы иметь смысл, если бы bбыло l-значением, но это был бы указатель, тогда как побитовое значение &потребовало бы целочисленного операнда, поэтому логический AND был бы единственным разумным выбором.

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

Christophe
источник
10
Анализатор не может устранить неоднозначность в двух ситуациях, поэтому разработчики языка должны сделать это.
BobDalgleish
16
@Steve: Действительно, в C и C-подобных языках уже есть много похожих проблем. Когда анализатор видит в (t)+1том , что добавление (t)и 1или это слепок +1типу t? C ++ дизайн должен был решить проблему с тем, чтобы лекс шаблоны содержали >>правильно. И так далее.
Эрик Липперт
6
@ user2357112 Я думаю, суть в том, что токенизатор можно брать вслепую &&как один &&токен, а не как два токена &, потому что a & (&b)интерпретация не является разумной вещью, поэтому человек никогда бы не подумал об этом и был бы удивлен компилятор рассматривает это как a && b. В то время как !(!a)и то и другое !!aмогут иметь значение для человека, поэтому для компилятора плохая идея разрешить неоднозначность с помощью произвольного правила уровня токенизации.
Бен
18
!!не только возможно / разумно написать, но каноническая идиома "преобразовать в логическое значение".
R ..
4
Я думаю, что dan04 относится к неоднозначности --avs -(-a), оба из которых допустимы синтаксически, но имеют разную семантику.
Руслан
49

Я не могу придумать причину, по которой дизайнеры решили отклониться от принципа, что одиночный битовый, а двойной логичный,

Это не принцип в первую очередь; как только вы поймете это, это станет более логичным.

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

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

Итак, давайте это вместе. Имеет ли смысл выполнять ленивую операцию с массивом логических значений ? Нет, потому что нет "левой стороны", чтобы проверить в первую очередь. Сначала нужно проверить 32 «левых стороны». Таким образом, мы ограничиваем ленивые операции одним булевым значением, и именно отсюда ваша интуиция, что одна из них является «двоичной», а другая «булевой», но это является следствием дизайна, а не самого проекта!

И когда вы думаете об этом таким образом, становится понятно, почему нет !!и нет ^^. Ни у одного из этих операторов нет свойства, которое можно пропустить, анализируя один из операндов; нет "ленивых" notили xor.

Другие языки делают это более ясным; некоторые языки используют andдля обозначения «нетерпеливый» и and also«ленивый», например. И другие языки также сделать его более ясным , что &и &&не «двоичный» и «Boolean»; например, в C # обе версии могут принимать логические значения в качестве операндов.

Эрик Липперт
источник
2
Спасибо. Это настоящая сенсация для меня. Жаль, что я не могу принять два ответа.
Мартин Маат
11
Я не думаю , что это хороший способ думать &и &&. В то время как усердие одна из отличий &и &&,& ведет себя совершенно иначе, чем рьяная версия &&, особенно в тех языках, где &&поддерживаются типы, отличные от выделенного логического типа.
user2357112 поддерживает Monica
14
Например, в C и C ++, 1 & 2имеет совершенно другой результат1 && 2 .
user2357112 поддерживает Monica
7
@ZizyArcher: Как я уже отмечал в комментарии выше, решение об исключении boolтипа в C имеет эффект " зацепки" . Нам нужны и то, !и другое, ~потому что одно означает «обрабатывать int как один логический тип», а другое означает «обрабатывать int как упакованный массив логических значений». Если у вас есть отдельные типы bool и int, то у вас может быть только один оператор, который, на мой взгляд, был бы лучше, но мы опоздали почти на 50 лет. C # сохраняет этот дизайн для ознакомления.
Эрик Липперт
3
@ Стив: Если ответ кажется абсурдным, я где-то привел плохо выраженный аргумент, и мы не должны полагаться на аргумент авторитета. Можете ли вы сказать больше о том, что кажется абсурдным?
Эрик Липперт
21

TL; DR

C унаследовал операторы !and ~от другого языка. Оба &&и ||были добавлены годами позже другим человеком.

Длинный ответ

Исторически, C развивался из ранних языков B, который был основан на BCPL, который был основан на CPL, который был основан на Algol.

Алгол , прадедушка C ++, Java и C #, определил истину и ложь таким образом, который стал понятен программистам: «Значения истины, которые рассматриваются как двоичное число (истина соответствует 1 и ложь - 0), такой же, как внутренняя интегральная стоимость ». Однако одним из недостатков этого является то, что логическое и побитовое не может быть одной и той же операцией: на любом современном компьютере~0 равно -1, а не 1, и ~1равно -2, а не 0. (Даже на каком-то шестидесятилетнем мэйнфрейме, где ~0представляет - 0 или INT_MIN, ~0 != 1на каждом процессоре когда - либо делал, и стандарт языка C потребовал его в течение многих лет, в то время как большинство его дочерние языков не удосужилось поддержка знаковой-и величина или one's-комплемента на всех.)

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

BCPL имел отдельный логический тип, но один notоператор , как для побитового, так и для логического типа. Способ, которым этот ранний предшественник Си сделал эту работу:

Rvalue истины - это битовый паттерн, полностью состоящий из единиц; R-значение false равно нулю.

Обратите внимание, что true = ~ false

(Вы заметите, что термин rvalue эволюционировал для обозначения чего-то совершенно другого в языках семейства C. Сегодня мы бы назвали это «представлением объекта» в C.)

Это определение позволит логически и поразрядно не использовать одну и ту же инструкцию машинного языка. Если бы C пошел по этому пути, заголовочные файлы по всему миру сказали бы#define TRUE -1 .

Но язык программирования B был слабо типизирован и не имел булевых или даже типов с плавающей точкой. Все было эквивалентомint в его преемнике, C. Это сделало для языка хорошей идеей определить, что происходит, когда программа использует значение, отличное от true или false, в качестве логического значения. Сначала он определил истинное выражение как «не равное нулю». Это было эффективно на миникомпьютерах, на которых он работал, с нулевым флагом ЦП.

В то время была альтернатива: те же процессоры также имели отрицательный флаг, и значение истинности BCPL было равно -1, так что B мог бы вместо этого определить все отрицательные числа как истинные, а все неотрицательные числа как ложные. (Есть один остаток этого подхода: UNIX, разработанная одними и теми же людьми в одно и то же время, определяет все коды ошибок как отрицательные целые числа. Многие из ее системных вызовов возвращают одно из нескольких различных отрицательных значений при сбое.) Так что будьте благодарны: это могло быть и хуже!

Но определение « TRUEкак» 1и « FALSEкак» 0в B означало, что тождество true = ~ falseбольше не сохранялось, и оно отбросило строгую типизацию, которая позволяла Algol устранять неоднозначность между битовыми и логическими выражениями. Это потребовало нового логического оператора not, и разработчики выбрали !, возможно, потому, что уже не равное было !=, которое выглядит как вертикальная черта через знак равенства. Они не следовали тому же соглашению, что &&и|| потому что ни один еще не существовал.

Возможно, они должны иметь: &оператор в B сломан, как задумано. В B и в C, 1 & 2 == FALSEхотя 1и 2являются истинными значениями, и нет никакого интуитивного способа выразить логическую операцию в B. Это была одна ошибка, которую C пытался частично исправить, добавив &&и ||, но основной проблемой в то время было наконец, установите короткое замыкание на работу и заставьте программы работать быстрее. Доказательством этого является то, что нет ^^: 1 ^ 2истинное значение, хотя оба его операнда истинны, но оно не может извлечь выгоду из короткого замыкания.

Davislor
источник
4
+1. Я думаю, что это довольно хорошая экскурсия по эволюции этих операторов.
Стив
Кстати, знак / величина и машины дополнения также должны разделяться по битам против логического отрицания, даже если входное значение уже логическое. ~0(все биты установлены) - это отрицательный ноль дополнения (или представление ловушки). Знак / величина ~0является отрицательным числом с максимальной величиной.
Питер Кордес
@PeterCordes Ты абсолютно прав. Я просто сосредоточился на машинах с двумя комплементами, потому что они намного важнее. Может быть, стоит сноска.
Дэвислор
Я думаю, что мой комментарий является достаточным, но да, возможно, хорошим редактированием будет скобка (не работает для дополнения 1 или знака / величины).
Питер Кордес