Pourquoi l'opérateur logique NOT dans les langages de style C est-il “!” Et non “~~”?

40

Pour les opérateurs binaires, nous avons à la fois des opérateurs au niveau du bit et des opérateurs logiques:

& bitwise AND
| bitwise OR

&& logical AND
|| logical OR

NOT (un opérateur unaire) se comporte toutefois différemment. Il y a ~ pour bitwise et! pour logique.

Je reconnais que NOT est une opération unaire par opposition à AND and OR, mais je ne vois pas pourquoi les concepteurs ont choisi de s’écarter du principe que simple est synonyme de bitwise et que double est logique ici, et a plutôt opté pour un caractère différent. J'imagine que vous pourriez le lire mal, comme une opération à deux bits qui renverrait toujours la valeur d'opérande. Mais cela ne me semble pas vraiment un problème.

Y a-t-il une raison qui me manque?

Martin Maat
la source
7
Parce que si !! ne voulais pas dire logique, comment pourrais-je transformer 42 en 1? :)
candied_orange
9
N'auriez-vous ~~pas alors été plus cohérent pour le NOT logique si vous suiviez le modèle selon lequel l'opérateur logique est un doublement de l'opérateur au niveau du bit?
Bart van Ingen Schenau le
9
Premièrement, si c’était pour la cohérence, cela aurait été ~ et ~~ Le doublement de et et ou est associé au court-circuit; et la logique n'a pas de court-circuit.
Christophe
3
Je soupçonne que la raison de conception sous-jacente est la clarté visuelle et la distinction, dans les cas d'utilisation typiques. Les opérateurs binaires (c'est-à-dire à deux opérandes) sont infixes (et ont tendance à être séparés par des espaces), tandis que les opérateurs unaires sont préfixés (et ont tendance à ne pas être espacés).
Steve
7
Comme certains commentaires ont déjà fait allusion (et pour ceux qui ne veulent pas suivre ce lien , !!fooest un idiome Il normalise non rare (non pas commun) un argument zéro ou non nul?. 0Ou 1.
Keith Thompson

Réponses:

110

Étrangement, l’histoire du langage de programmation de type C ne commence pas par C.

Dennis Ritchie explique bien les défis de la naissance de C dans cet article .

En le lisant, il devient évident que C a hérité une partie de la conception de son langage de son prédécesseur, BCPL , et en particulier des opérateurs. La section « néonatale C » de l'article mentionné ci - dessus explique comment son BCPL &et |ont été enrichis avec deux nouveaux opérateurs &&et ||. Les raisons étaient:

  • priorité différente était nécessaire en raison de son utilisation en combinaison avec ==
  • Logique d'évaluation différente: évaluation de gauche à droite avec court-circuit (c'est -à- dire quand aest falsedans a&&b, bn'est pas évalué).

Fait intéressant, ce dédoublement ne crée aucune ambiguïté pour le lecteur: a && bne sera pas mal interprété comme a(&(&b)). D'un point de vue analytique , il n'y a pas d'ambiguïté non plus: cela &bpourrait avoir un sens s'il bs'agissait d'une lvalue, mais ce serait un pointeur alors que le bitwise &nécessiterait un opérande entier, le AND logique serait donc le seul choix raisonnable.

BCPL est déjà utilisé ~pour la négation au niveau du bit. Donc, d’un point de vue de la cohérence, on aurait pu le doubler pour lui ~~donner son sens logique. Malheureusement, cela aurait été extrêmement ambigu, car ~c’est un opérateur unaire: cela ~~bpourrait aussi vouloir dire ~(~b)). C'est pourquoi il a fallu choisir un autre symbole pour la négation manquante.

Christophe
la source
10
L'analyseur n'est pas en mesure de lever l'ambiguïté des deux situations, les concepteurs de langage doivent le faire.
BobDalgleish le
16
@Steve: En effet, de nombreux problèmes similaires existent déjà dans les langages C et C. Lorsque l'analyseur voit (t)+1est qu'une addition de (t)et 1ou est - ce un casting de +1type t? La conception C ++ devait résoudre le problème de la syntaxe >>correcte des modèles . Etc.
Eric Lippert le
6
@ user2357112 Je pense que le problème, c'est qu'il est normal que le tokenizer prenne aveuglément &&comme un &&jeton unique et non comme deux &jetons, car l' a & (&b)interprétation n'est pas une chose raisonnable à écrire. Un humain n'aurait jamais voulu dire cela et aurait été surpris par le compilateur le traite comme a && b. Tandis que les deux !(!a)et !!asont des choses possibles pour un humain, c'est donc une mauvaise idée pour le compilateur de résoudre l'ambiguïté avec une règle de niveau de symbolisation arbitraire.
Ben
18
!!n’est pas seulement possible / raisonnable d’écrire, mais l’idiome canonique "convertir en booléen".
R ..
4
Je pense que dan04 fait référence à l'ambiguïté de --avs -(-a), qui sont tous deux syntaxiquement valables mais ont une sémantique différente.
Ruslan
49

Je ne vois aucune raison pour laquelle les concepteurs ont choisi de s’écarter du principe selon lequel simple est au niveau des bits et double est logique ici,

Ce n'est pas le principe en premier lieu; une fois que vous vous en rendez compte, c'est plus logique.

La meilleure façon de penser à &vs &&n'est pas binaire ni booléenne . Le meilleur moyen est de penser à eux comme impatients et paresseux . L' &opérateur exécute les côtés gauche et droit, puis calcule le résultat. L' &&opérateur exécute le côté gauche, puis n'exécute le côté droit que si nécessaire pour calculer le résultat.

De plus, au lieu de penser à "binaire" et "booléen", pensez à ce qui se passe réellement. La version "binaire" ne fait que l’opération booléenne sur un tableau de booléens qui a été condensé dans un mot .

Alors mettons-le ensemble. Est-il logique de faire une opération paresseuse sur un tableau de booléens ? Non, car il n'y a pas de "côté gauche" à vérifier en premier. Il y a 32 "côtés gauche" à vérifier en premier. Nous limitons donc les opérations paresseuses à un seul booléen, et c’est de là que vient votre intuition selon laquelle l’un d’eux est «binaire» et l’autre «Booléen», mais c’est une conséquence. du design, pas du design lui-même!

Et quand on y pense de cette façon, on comprend pourquoi il n’ya !!ni non ni non ^^. Aucun de ces opérateurs n'a la propriété que vous pouvez ignorer en analysant l'un des opérandes; il n'y a pas de "paresseux" notou xor.

D'autres langues rendent cela plus clair. certaines langues utilisent andle sens "désireux et" mais and alsosignifient "paresseux et", par exemple. Et d'autres langues précisent également que &et &&ne sont pas "binaires" et "booléennes"; en C # par exemple, les deux versions peuvent prendre les booléens comme opérandes.

Eric Lippert
la source
2
Merci. Ceci est la véritable révélation pour moi. Dommage que je ne puisse pas accepter deux réponses.
Martin Maat le
11
Je ne pense pas que ce soit une bonne façon de penser &et &&. Bien que l’empressement soit l’une des différences entre &et &&, &se comporte de manière totalement différente d’une version désireuse de &&, en particulier dans les langues où &&les types de support autres que le type booléen dédié sont pris en charge.
user2357112 prend en charge Monica
14
Par exemple, en C et C ++, le 1 & 2résultat obtenu est complètement différent 1 && 2.
user2357112 prend en charge Monica
7
@ZizyArcher: Comme je l'ai noté dans le commentaire ci-dessus, la décision d'omettre un booltype en C a des effets d'entraînement. Nous avons besoin des deux !et ~parce que l’un veut dire "traiter un int comme un booléen unique" et l’un pour "traiter un int comme un tableau rempli de booléens". Si vous avez des types bool et int distincts, vous ne pouvez avoir qu'un seul opérateur, ce qui, à mon avis, aurait été la meilleure conception, mais nous avons presque 50 ans de retard pour celui-là. C # conserve cette conception pour la familiarité.
Eric Lippert
3
@Steve: Si la réponse semble absurde, j’ai formulé un argument mal exprimé quelque part, et nous ne devrions pas nous en remettre à un argument émanant de l’autorité. Pouvez-vous en dire plus sur ce qui semble absurde à ce sujet?
Eric Lippert
21

TL; DR

C a hérité des opérateurs !and ~d’une autre langue. Les deux &&et ||ont été ajoutés des années plus tard par une personne différente.

Longue réponse

Historiquement, C s'est développé à partir des premières langues B, qui étaient basées sur BCPL, qui était basé sur CPL, qui était basé sur Algol.

Algol , l'arrière-petit-père de C ++, Java et C #, a défini vrai et faux de manière intuitive pour les programmeurs: «valeurs de vérité qui, considérées comme un nombre binaire (vrai correspondant à 1 et faux à 0), sont identique à la valeur intrinsèque intrinsèque ». Cependant, un inconvénient de cela est que logique et que bit à bit ne peut pas être la même opération: sur tout ordinateur moderne, ~0égal à -1 plutôt que 1 et ~1égal à -2 plutôt que 0 (même sur un ordinateur central vieux de soixante ans où ~0représente - 0 ou INT_MIN, ~0 != 1sur chaque processeur jamais créé, et la norme de langage C l’exige depuis de nombreuses années, alors que la plupart des langues de sa fille ne se soucient même pas de prendre en charge le principe de complémentarité.

Algol a contourné cela en ayant différents modes et en interprétant les opérateurs différemment en mode booléen et intégral. C'est-à-dire qu'une opération au niveau des bits était une opération sur les types entiers et une opération logique, une opération sur des types booléens.

BCPL avait un type booléen distinct, mais un seul notopérateur , à la fois au niveau du bit et logique. La manière dont ce précurseur précoce de C a réalisé ce travail était:

La valeur de true est un modèle de bits entièrement composé de uns; la Rvalue de false est zéro.

Notez que true = ~ false

(Vous remarquerez que le terme rvalue a évolué pour signifier quelque chose de complètement différent dans les langues de la famille C. Nous appellerions aujourd'hui cela «la représentation d'objet» en C).

Cette définition permettrait à logique et au bit de ne pas utiliser la même instruction en langage machine. Si C avait choisi cette voie, diraient les fichiers d’en-tête du monde entier #define TRUE -1.

Mais le langage de programmation B était faiblement typé et n'avait pas de type booléen ni même de type virgule flottante. Tout était l'équivalent de intson successeur, C. Cela incitait le langage à définir ce qui se passait lorsqu'un programme utilisait une valeur autre que true ou false comme valeur logique. Il a d'abord défini une expression de vérité comme étant «non égale à zéro». Cette méthode était efficace sur les mini-ordinateurs sur lesquels il fonctionnait, qui présentaient un indicateur de zéro CPU.

Il y avait à l'époque une alternative: les mêmes processeurs avaient également un drapeau négatif et la valeur de vérité de BCPL était -1, alors B aurait peut-être défini tous les nombres négatifs comme des vrais et tous les nombres non négatifs comme des faux. (Il y a un reste de cette approche: UNIX, développé par les mêmes personnes en même temps, définit tous les codes d'erreur en tant qu'entiers négatifs. Beaucoup de ses appels système renvoient l'une de plusieurs valeurs négatives différentes en cas d'échec.) Soyez donc reconnaissant: il Aurait pu être pire!

Mais définir TRUEcomme 1et FALSEcomme 0dans B signifiait que l’identité true = ~ falsen’était plus conservée et que le typage fort qui permettait à Algol de ne pas être ambiguïté entre les expressions binaires et logiques disparaissait. Cela nécessitait un nouvel opérateur logique-non, et les concepteurs ont choisi !, peut-être parce que non-égal-à était déjà !=, qui ressemble à une barre verticale à travers un signe égal. Ils n'ont pas suivi la même convention &&ou ||parce que ni l'un ni l'autre n'existaient encore.

On peut soutenir qu'ils devraient avoir: l' &opérateur en B est cassé comme prévu. En B et en C, 1 & 2 == FALSEmême si 1et 2sont deux valeurs véridiques, et il n’existe aucun moyen intuitif d’exprimer l’opération logique en B. C’est une erreur que C a tenté de rectifier en partie en ajoutant &&et ||, mais la principale préoccupation à l’époque était de: enfin, faites fonctionner les courts-circuits et accélérez les programmes. La preuve est qu'il n'y a pas ^^: 1 ^ 2est une valeur truthy même si ses deux opérandes sont truthy, mais il ne peut pas bénéficier de court-circuit.

Davislor
la source
4
+1 Je pense que c'est une très bonne visite guidée de l'évolution de ces opérateurs.
Steve
Les machines BTW, signe / magnitude et son complément ont également besoin d'une négation bit à bit vs logique, même si l'entrée est déjà booléenne. ~0(tous les bits sont définis) est le complément à zéro négatif de son complément (ou une représentation de piège). Le signe / magnitude ~0est un nombre négatif avec une magnitude maximale.
Peter Cordes
@PeterCordes Vous avez absolument raison. Je me concentrais uniquement sur les machines à deux, car elles sont beaucoup plus importantes. Cela vaut peut-être une note de bas de page.
Davislor
Je pense que mon commentaire est suffisant, mais oui, peut-être qu'une parenthèse (ne fonctionne pas pour le complément à 1 ou le signe / grandeur) serait un bon montage.
Peter Cordes