Чому логічний оператор НЕ в мовах стилю С "!", А не "~~"?


39

Для двійкових операторів у нас є як бітові, так і логічні оператори:

& bitwise AND
| bitwise OR

&& logical AND
|| logical OR

НЕ (одинарний оператор) поводиться по-іншому. Існує ~ для розрядних і! для логічного.

Я визнаю, що НЕ є одинарною операцією на відміну від AND і OR, але я не можу придумати причину, чому дизайнери вирішили відхилитися від принципу, що сингл є побітним і подвійним, тут логічно, і замість цього пішов інший персонаж. Я думаю, ви могли прочитати це неправильно, як подвійну бітову операцію, яка б завжди повертала значення операнду. Але це не здається мені справжньою проблемою.

Чи є причина, яку я пропускаю?


7
Тому що якщо !! означало, що це не логічно, як я перетворив 42 на 1? :)
candied_orange

9
Б ~~то не було більш послідовним для логічного NOT, якщо ви будете слідувати шаблоном , що логічний оператор є подвоєнням оператора побітового?
Барт ван Інген Шенау

9
По-перше, якби це було для консистенції, це було б ~ і ~~ Подвоєння та і або пов'язане з коротким замиканням; і логічний не має короткого замикання.
Крістоф

3
Я підозрюю, що основна причина дизайну - це наочна чіткість та відмінність у типових випадках використання. Двійкові (тобто двооперандні) оператори інфіксуються (і, як правило, розділяються пробілами), тоді як одинарні оператори є префіксом (і, як правило, не розташовуються між собою).
Стів

7
Як уже згадували деякі коментарі (і для тих, хто не хоче переходити за цим посиланням , !!fooце нечаста (не звичайна?) Ідіома. Вона нормалізує нульовий або ненульовий аргумент до 0або 1.
Кіт Томпсон

Відповіді:


108

Як не дивно, історія мови програмування у стилі С не починається з C.

Денніс Рітчі добре пояснюють проблеми народження мови C в цій статті .

Читаючи його, стає очевидним, що C успадкував частину свого мовного дизайну від свого попередника BCPL , а особливо операторів. У розділі «Неонати С» вищезгаданої статті пояснюється, як BCPL &і |збагачувалися двома новими операторами &&та ||. Причини:

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

Цікаво, що це подвоєння не створює для читача ніякої неоднозначності: a && bне буде неправильно трактуватися як a(&(&b)). З точки зору розбору, також немає двозначності: це &bмогло б мати сенс, якби bбуло значення, але це було б покажчиком, тоді як побітовий порядок &потребував би цілого операнда, тому логічний AND був би єдиним розумним вибором.

BCPL вже використовується ~для побітового заперечення. Отже, з точки зору послідовності, його можна було б подвоїти, ~~щоб надати йому логічного значення. На жаль, це було б вкрай неоднозначно, оскільки ~є одинарним оператором: ~~bце також може означати ~(~b)). Ось чому для відсутнього заперечення потрібно було обрати інший символ.


10
Аналізатор не в змозі розмежувати дві ситуації, тому мовні дизайнери повинні це зробити.
BobDalgleish

16
@Steve: Дійсно, є багато подібних проблем вже на мовах С та С-подібних. Коли аналізатор бачить (t)+1, що це доповнення (t)та 1чи це вступ +1для набору t? Дизайн C ++ повинен був вирішити проблему, як правильно використовувати лексичні шаблони >>. І так далі.
Ерік Ліпперт

6
@ user2357112 Я думаю, що справа в тому, що це нормально, щоб токенізатор сліпо сприймав &&як один &&маркер, а не як два &лексеми, тому що a & (&b)інтерпретація не є розумною справою для написання, тому людина ніколи б не означала цього і дивувалась компілятор, що трактує це як a && b. Тоді як !(!a)і !!aлюдські люди можуть означати те, що може означати, тому компілятор погано задумає вирішити двозначність за допомогою довільного правила рівня токенізації.
Бен

17
!!не тільки можна / розумно писати, але канонічну "перетворювати на булева" ідіому.
Р ..

4
Я думаю, що dan04 має на увазі неоднозначність --avs -(-a), обидва вони є синтаксичними, але мають різну семантику.
Руслан

49

Я не можу придумати причину, чому дизайнери вирішили відхилитися від принципу, що сингл є побітним і подвійним, тут логічно,

Це не принцип в першу чергу; як тільки ви зрозумієте це, це має більше сенсу.

Кращий спосіб думати &проти &&- це не бінарний та булевий . Кращий спосіб - думати про них як прагнучих і ледачих . &Оператор виконує ліву і праву сторону , а потім обчислює результат. &&Оператор виконує ліву сторону, а потім виконує тільки з правого боку , якщо це необхідно , щоб обчислити результат.

Більше того, замість того, щоб думати про "бінарне" та "булеве", подумайте, що насправді відбувається. "Бінарна" версія просто виконує булеву операцію на масиві булевих, який був упакований у слово .

Тож давайте складемо це разом. Чи є сенс робити ліниву операцію на масиві булевих ? Ні, тому що немає "лівої сторони", щоб перевірити спочатку. Є 32 "ліві сторони", щоб перевірити спочатку. Тож ми обмежуємо ледачі операції одним булевим, і саме звідси походить ваша інтуїція, що одна з них "бінарна", а "булева", але це наслідок дизайну, а не самого дизайну!

І коли ви думаєте про це так, стає зрозуміло, чому немає !!і ні ^^. Жоден з цих операторів не має властивості, яку ви можете пропустити, аналізуючи один із операндів; немає "ледачих" notчи xor.

Інші мови роблять це більш зрозумілим; деякі мови вживають, наприклад, and"нетерплячий", але, наприклад, and also"ледачий". І інші мови також зробити його більш ясним , що &і &&не "двійковий» і «Boolean»; Наприклад, у C #, обидві версії можуть приймати булеви як операнди.


2
Дякую. Це справжній відкривач для очей для мене. Шкода, що я не можу прийняти дві відповіді.
Мартін Мейт

10
Я не думаю, що це хороший спосіб думати &і &&. Незважаючи на те, що жадібність є однією з відмінностей між &і &&, &веде себе зовсім інакше, ніж у нетерплячої версії &&, особливо в мовах, де &&підтримуються типи, відмінні від виділеного булевого типу.
user2357112 підтримує Моніку

14
Наприклад, для C і C ++ 1 & 2має абсолютно інший результат від 1 && 2.
user2357112 підтримує Моніку

7
@ZizyArcher: Як я зазначав у коментарі вище, рішення про опущення boolтипу в C має стукотні ефекти. Нам потрібно і те, !і ~тому, що один означає "ставитись до int як до єдиного булевого", а один означає "ставитись до int як до упакованого масиву булевих". Якщо у вас є окремі типи bool та int, то ви можете мати лише одного оператора, що, на мою думку, було б кращим дизайном, але ми на це майже запізнилися на 50 років. C # зберігає цю конструкцію для знайомства.
Ерік Ліпперт

3
@Steve: Якщо відповідь здається абсурдною, я десь висловив слабко виражений аргумент, і нам не слід покладатися на аргумент влади. Чи можете ви сказати більше про те, що в цьому здається абсурдним?
Ерік Ліпперт

21

TL; DR

C успадкував оператори !і ~з іншої мови. І те, &&і ||інше було додане роками пізніше.

Довга відповідь

Історично C розвивався з ранніх мов B, що базувався на BCPL, який базувався на CPL, який базувався на Algol.

Алгол , правнук C ++, Java та C #, визначив істинне та хибне таким чином, щоб зрозуміти програмістам інтуїтивно зрозуміло: "значення істини, які, розглядаються як двійкове число (істинне, що відповідає 1, а хибне 0), те саме, що і внутрішня цілісна цінність ”. Однак одним недоліком цього є те, що логічна та бітова не може бути однаковою операцією. На будь-якому сучасному комп’ютері ~0дорівнює -1, а не 1, і ~1дорівнює -2, а не 0. (Навіть на якомусь шестидесятирічному мейнфреймі, де ~0представлено - 0 або INT_MIN, ~0 != 1будь-який процесор, коли-небудь виготовлений, і стандарт мови C вимагає цього протягом багатьох років, тоді як більшість його дочірніх мов навіть не намагаються підтримувати знаки та величини або доповнення взагалі.)

Альгол обійшов це, маючи різні режими та інтерпретуючи операторів по-різному в бульному та інтегральному режимі. Тобто, побітова операція була одиницею на цілі типи, а логічна операція - на булевих типах.

BCPL мав окремий булівський тип, але єдиний notоператор , як для побітових, так і для логічних даних. Те, як цей ранній провісник С зробив цю роботу:

Rvalue істинного - це біт-шаблон, повністю складається з них; Rvalue false - нуль.

Зауважте, що true = ~ false

(Ви помітите, що термін rvalue перетворився на щось зовсім інше в мовах сімейства C. Сьогодні ми би назвали це "представлення об'єкта" в C.)

Це визначення дозволило б логічно і побіжно не використовувати однакові інструкції на машинній мові. Якби C пройшов цей маршрут, заголовки надсилали б весь світ #define TRUE -1.

Але мова програмування B була слабо типованою і не мала булевих або навіть типів з плаваючою комою. Все було еквівалентом intйого наступника. Це дозволило мові визначити те, що сталося, коли програма використовувала як логічне значення значення, відмінне від істинного чи помилкового. Вперше він визначив виразний вираз як "не дорівнює нулю". Це було ефективно для міні-комп'ютерів, на яких він працював, у яких був прапор CPU нульового рівня.

На той час була альтернатива: ті ж процесори також мали негативний прапор, а значення правди BCPL було -1, тож B міг би замість цього визначити всі негативні числа як істинні, а всі негативні числа - як хибні. (Є один залишок такого підходу: багато системних викликів в UNIX, розроблених одночасно одними людьми, визначає всі коди помилок як від'ємні цілі числа. Багато системних викликів повертають одне з декількох різних від'ємних значень при відмові.) будьте вдячні: могло бути і гірше!

Але визначення TRUEяк 1і FALSEяк 0в B означало, що особистість true = ~ falseбільше не тримається, і це відкинуло сильну типізацію, яка дозволила Альголу розмежуватись між бітовими та логічними виразами. Для цього потрібен новий логічний оператор, і дизайнери вибрали !, можливо, тому, що не було рівним-вже !=, що виглядає як вертикальна смуга через знак рівності. Вони не дотримувались тієї ж конвенції, &&або ||тому, що жодної ще не існувала.

Можливо, у них повинно бути: &оператор в B порушений, як було заплановано. В B і C, 1 & 2 == FALSEхоча 1і 2є обидві значення, і немає інтуїтивного способу виразити логічну операцію в B. Це була одна помилка, яку С намагався частково виправити додаванням &&і ||, але головна проблема в той час полягала в тому, щоб нарешті, запускайте коротке замикання на роботу та змушуйте програми працювати швидше. Підтвердженням цього є те, що немає ^^: 1 ^ 2значення truthy, хоча обидва його операнда є truthy, але він не може отримати користь від короткого замикання.


4
+1. Я думаю, що це досить гарна екскурсія навколо еволюції цих операторів.
Стів

BTW, знак / величина та доповнення машин також потребують окремого побітового та логічного заперечення, навіть якщо вхід уже булеанізований. ~0(усі набори бітів) - це додатковий нульовий додаток (або подання пастки). Знак / величина ~0- це від’ємне число з максимальною величиною.
Пітер Кордес

@PeterCordes Ви абсолютно праві. Я просто зосередився на машинах, що доповнюють два, тому що вони набагато важливіші. Можливо, це варто виноски.
Девіслор

Я думаю, що мій коментар є достатнім, але так, можливо, дужка (це не працює для 1-го доповнення або знаку / величини) було б гарним редагуванням.
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.