== et! = Sont-ils mutuellement dépendants?

292

J'apprends la surcharge des opérateurs en C ++, et je le vois ==et ce !=sont simplement des fonctions spéciales qui peuvent être personnalisées pour les types définis par l'utilisateur. Cependant, je me demande pourquoi deux définitions distinctes sont nécessaires? Je pensais que si a == best vrai, alors a != bautomatiquement faux, et vice versa, et il n'y a pas d'autre possibilité, parce que, par définition, l' a != best !(a == b). Et je ne pouvais imaginer aucune situation dans laquelle ce n'était pas vrai. Mais peut-être que mon imagination est limitée ou que j'ignore quelque chose?

Je sais que je peux définir l'un en fonction de l'autre, mais ce n'est pas ce que je demande. Je ne demande pas non plus la distinction entre comparer des objets par valeur ou par identité. Ou si deux objets pourraient être égaux et non égaux en même temps (ce n'est certainement pas une option! Ces choses s'excluent mutuellement). Ce que je demande, c'est ceci:

Y at - il possible dans la situation où poser des questions sur deux objets étant ne égale du sens, mais poser des questions sur les pas être ne correspond pas de sens? (soit du point de vue de l'utilisateur, soit du point de vue de l'implémentateur)

S'il n'y a pas une telle possibilité, alors pourquoi diable C ++ at-il ces deux opérateurs définis comme deux fonctions distinctes?

BarbaraKwarc
la source
13
Deux pointeurs peuvent tous deux être nuls mais pas nécessairement égaux.
Ali Caglayan
2
Je ne sais pas si cela a du sens ici, mais lire cela m'a fait penser à des problèmes de «court-circuit». Par exemple, on pourrait définir que 'undefined' != expressionc'est toujours vrai (ou faux ou non défini), que l'expression puisse être évaluée ou non. Dans ce cas a!=b, renvoie le résultat correct selon la définition, mais !(a==b)échoue s'il bne peut pas être évalué. (Ou prenez beaucoup de temps si l'évaluation bcoûte cher).
Dennis Jaheruddin
2
Et null! = Null et null == null? Cela peut être les deux ... donc si a! = B cela ne signifie pas toujours a == b.
zozo
4
Un exemple de javascript(NaN != NaN) == true
chiliNUT

Réponses:

272

Vous ne voudriez pas que la langue se réécrive automatiquement a != bcomme !(a == b)lorsqu'elle a == bretourne autre chose qu'un a bool. Et il y a plusieurs raisons pour lesquelles vous pourriez le faire.

Vous pouvez avoir des objets de générateur d'expression, où a == bne fait pas et n'est pas destiné à effectuer une comparaison, mais construit simplement un nœud d'expression représentant a == b.

Vous pouvez avoir une évaluation paresseuse, où a == bn'a pas et n'est pas destiné à effectuer une comparaison directement, mais renvoie à la place une sorte de lazy<bool>cela qui peut être converti boolimplicitement ou explicitement à un moment ultérieur pour effectuer réellement la comparaison. Peut être combiné avec les objets du générateur d'expression pour permettre une optimisation complète de l'expression avant l'évaluation.

Vous pouvez avoir une optional<T>classe de modèle personnalisée , où des variables facultatives sont fournies tet que uvous souhaitez autoriser t == u, mais faites-la revenir optional<bool>.

Il y a probablement plus de choses auxquelles je n'ai pas pensé. Et même si, dans ces exemples, l'opération a == bet a != bles deux ont un sens, ce a != bn'est toujours pas la même chose !(a == b), des définitions distinctes sont donc nécessaires.


la source
72
La construction d'expressions est un exemple pratique fantastique de quand vous le souhaitez, qui ne dépend pas de scénarios artificiels.
Oliver Charlesworth
6
Un autre bon exemple serait les opérations logiques vectorielles. Vous préférez un passage à travers les données de calcul au !=lieu de deux passes de calcul ==alors !. Surtout à l'époque où vous ne pouviez pas compter sur le compilateur pour fusionner les boucles. Ou même aujourd'hui, si vous ne parvenez pas à convaincre le compilateur, vos vecteurs ne se chevauchent pas.
41
"Vous pouvez avoir des objets de générateur d'expression" - eh bien, l'opérateur !peut également créer un nœud d'expression et nous pouvons toujours le remplacer a != bpar !(a == b), pour autant que cela se passe. Il en va de même lazy<bool>::operator!, il peut revenir lazy<bool>. optional<bool>est plus convaincant, car la véracité logique de, par exemple, boost::optionaldépend de l'existence d'une valeur et non de la valeur elle-même.
Steve Jessop
42
Tout cela, et Nans - s'il vous plaît rappelez-vous les NaNs;
jsbueno
9
@jsbueno: il a été souligné plus loin que les NaN ne sont pas spéciaux à cet égard.
Oliver Charlesworth
110

S'il n'y a pas une telle possibilité, alors pourquoi diable C ++ at-il ces deux opérateurs définis comme deux fonctions distinctes?

Parce que vous pouvez les surcharger, et en les surchargeant, vous pouvez leur donner un sens totalement différent de leur original.

Prenons, par exemple, l'opérateur <<, à l'origine l'opérateur de décalage gauche au niveau du bit, maintenant généralement surchargé en tant qu'opérateur d'insertion, comme dans std::cout << something; sens totalement différent de celui d'origine.

Donc, si vous acceptez que la signification d'un opérateur change lorsque vous le surchargez, il n'y a aucune raison d'empêcher l'utilisateur de donner à l'opérateur une signification ==qui n'est pas exactement la négation de l'opérateur !=, bien que cela puisse prêter à confusion.

pie
la source
18
C'est la seule réponse qui ait un sens pratique.
Sonic Atom
2
Il me semble que vous avez la cause et l'effet à l'envers. Vous pouvez les surcharger séparément car ==et !=exister en tant qu'opérateurs distincts. D'un autre côté, ils n'existent probablement pas en tant qu'opérateurs distincts car vous pouvez les surcharger séparément, mais pour des raisons d'héritage et de commodité (brièveté du code).
nitro2k01
60

Cependant, je me demande pourquoi deux définitions distinctes sont nécessaires?

Vous n'avez pas besoin de définir les deux.
S'ils s'excluent mutuellement, vous pouvez toujours être concis en ne définissant ==et à <côté de std :: rel_ops

Fom cppreference:

#include <iostream>
#include <utility>

struct Foo {
    int n;
};

bool operator==(const Foo& lhs, const Foo& rhs)
{
    return lhs.n == rhs.n;
}

bool operator<(const Foo& lhs, const Foo& rhs)
{
    return lhs.n < rhs.n;
}

int main()
{
    Foo f1 = {1};
    Foo f2 = {2};
    using namespace std::rel_ops;

    //all work as you would expect
    std::cout << "not equal:     : " << (f1 != f2) << '\n';
    std::cout << "greater:       : " << (f1 > f2) << '\n';
    std::cout << "less equal:    : " << (f1 <= f2) << '\n';
    std::cout << "greater equal: : " << (f1 >= f2) << '\n';
}

Y a-t-il une situation possible dans laquelle poser des questions sur deux objets égaux a du sens, mais poser des questions sur leur égalité n'a pas de sens?

Nous associons souvent ces opérateurs à l'égalité.
Bien que c'est ainsi qu'ils se comportent sur les types fondamentaux, il n'y a aucune obligation que ce soit leur comportement sur les types de données personnalisés. Vous n'avez même pas besoin de retourner un bool si vous ne le souhaitez pas.

J'ai vu des gens surcharger les opérateurs de manière bizarre, seulement pour découvrir que cela avait du sens pour leur application spécifique au domaine. Même si l'interface semble montrer qu'elles s'excluent mutuellement, l'auteur peut vouloir ajouter une logique interne spécifique.

(soit du point de vue de l'utilisateur, soit du point de vue de l'implémentateur)

Je sais que vous voulez un exemple spécifique,
alors voici celui du cadre de test Catch que je pensais pratique:

template<typename RhsT>
ResultBuilder& operator == ( RhsT const& rhs ) {
    return captureExpression<Internal::IsEqualTo>( rhs );
}

template<typename RhsT>
ResultBuilder& operator != ( RhsT const& rhs ) {
    return captureExpression<Internal::IsNotEqualTo>( rhs );
}

Ces opérateurs font des choses différentes, et il ne serait pas logique de définir une méthode comme un! (Pas) de l'autre. La raison pour laquelle cela est fait, est que le cadre puisse imprimer la comparaison effectuée. Pour ce faire, il doit capturer le contexte de l'opérateur surchargé utilisé.

Trevor Hickey
la source
14
Oh mon Dieu, comment pourrais-je ne pas savoir std::rel_ops? Merci beaucoup de l'avoir signalé.
Daniel Jour
5
Les copies quasi-verbatim de cppreference (ou ailleurs) doivent être clairement marquées et correctement attribuées. rel_opsest horrible de toute façon.
TC
@TC D'accord, je dis simplement que c'est une méthode que OP peut prendre. Je ne sais pas comment expliquer rel_ops plus simple que l'exemple montré. J'ai lié à l'endroit où il se trouve, mais j'ai publié du code car la page de référence pouvait toujours changer.
Trevor Hickey
4
Vous devez toujours indiquer clairement que l'exemple de code provient à 99% de cppreference plutôt que le vôtre.
TC
2
Std :: relops semble être tombé en disgrâce. Découvrez les opérations de boost pour quelque chose de plus ciblé.
JDługosz
43

Il existe des conventions très bien établies dans lesquelles (a == b)et qui (a != b)sont toutes deux fausses ne sont pas nécessairement opposées. En particulier, en SQL, toute comparaison avec NULL donne NULL, pas vrai ou faux.

Ce n'est probablement pas une bonne idée de créer de nouveaux exemples si cela est possible, car c'est si peu intuitif, mais si vous essayez de modéliser une convention existante, il est agréable d'avoir la possibilité de faire en sorte que vos opérateurs se comportent "correctement" pour cela le contexte.

Jander
la source
4
Vous implémentez un comportement nul de type SQL en C ++? Ewwww. Mais je suppose que ce n'est pas quelque chose que je pense devrait être interdit dans la langue, aussi désagréable que cela puisse être.
1
@ dan1111 Plus important encore, certaines versions de SQL peuvent très bien être codées en c ++, donc le langage doit prendre en charge leur syntaxe, non?
Joe
1
Corrigez-moi si je me trompe, je quitte juste wikipedia ici, mais la comparaison avec une valeur NULL dans SQL ne renvoie-t-elle pas inconnue, pas fausse? Et la négation d'Inconnu n'est-elle pas encore inconnue? Donc, si la logique SQL était codée en C ++, ne voudriez-vous pas NULL == somethingretourner Unknown, et vous voudriez aussi NULL != somethingretourner Unknown, et vous voudriez !Unknownrevenir Unknown. Et dans ce cas, l'implémentation operator!=comme la négation de operator==est toujours correcte.
Benjamin Lindley
1
@Barmar: D'accord, mais comment cela peut-il rendre correcte la déclaration "SQL NULLs this way" ? Si nous limitons nos implémentations d'opérateurs de comparaison au retour de booléens, cela ne signifie-t-il pas simplement que l'implémentation de la logique SQL avec ces opérateurs est impossible?
Benjamin Lindley
2
@Barmar: Eh bien non, ce n'est pas le but. Le PO le sait déjà, ou cette question n'existerait pas. Il s'agissait de présenter un exemple où il était logique 1) de mettre en œuvre l'un operator==ou operator!=, mais pas l'autre, ou 2) de mettre operator!=en œuvre d'une manière autre que la négation de operator==. Et l'implémentation de la logique SQL pour les valeurs NULL n'est pas un cas de cela.
Benjamin Lindley
23

Je ne répondrai qu'à la deuxième partie de votre question, à savoir:

S'il n'y a pas une telle possibilité, alors pourquoi diable C ++ at-il ces deux opérateurs définis comme deux fonctions distinctes?

L'une des raisons pour lesquelles il est logique de permettre au développeur de surcharger les deux est la performance. Vous pouvez permettre des optimisations en implémentant à la fois ==et !=. Alors, x != yça pourrait être moins cher que ça !(x == y). Certains compilateurs peuvent être en mesure de l'optimiser pour vous, mais peut-être pas, surtout si vous avez des objets complexes avec beaucoup de branchements impliqués.

Même à Haskell, où les développeurs prennent très au sérieux les lois et les concepts mathématiques, on est toujours autorisé à surcharger les deux ==et /=, comme vous pouvez le voir ici ( http://hackage.haskell.org/package/base-4.9.0.0/docs/Prelude .html # v: -61--61- ):

$ ghci
GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
λ> :i Eq
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
        -- Defined in `GHC.Classes'

Cela serait probablement considéré comme une micro-optimisation, mais cela pourrait être justifié dans certains cas.

Centril
la source
3
Les classes d'encapsuleur SSE (x86 SIMD) en sont un excellent exemple. Il y a une pcmpeqbinstruction, mais aucune instruction de comparaison compacte produisant un masque! =. Donc, si vous ne pouvez pas simplement inverser la logique de ce qui utilise les résultats, vous devez utiliser une autre instruction pour l'inverser. (Fait amusant: le jeu d'instructions XOP d'AMD a une comparaison neqcompacte. Dommage qu'Intel n'ait pas adopté / étendu XOP; il y a quelques instructions utiles dans cette extension ISA qui sera bientôt morte.)
Peter Cordes
1
Tout l'intérêt de SIMD en premier lieu est la performance, et vous ne prenez généralement la peine de l'utiliser que manuellement dans des boucles qui sont importantes pour la performance globale. L'enregistrement d'une seule instruction ( PXORavec tout-pour inverser le résultat du masque de comparaison) dans une boucle serrée peut être important.
Peter Cordes
La performance en tant que raison n'est pas crédible lorsque la surcharge est une négation logique .
Bravo et hth. - Alf
Il peut s'agir de plusieurs négations logiques si le calcul x == ycoûte beaucoup plus cher x != y. Le calcul de ce dernier pourrait être beaucoup moins cher en raison de la prédiction de branche, etc.
Centril
16

Y a-t-il une situation possible dans laquelle poser des questions sur deux objets égaux a du sens, mais poser des questions sur leur égalité n'a pas de sens? (soit du point de vue de l'utilisateur, soit du point de vue de l'implémentateur)

Voilà une opinion. Peut-être que non. Mais les concepteurs de langage, n'étant pas omniscients, ont décidé de ne pas restreindre les personnes qui pourraient trouver des situations dans lesquelles cela pourrait avoir du sens (du moins pour eux).

Benjamin Lindley
la source
13

En réponse à l'édition;

Autrement dit, s'il est possible pour certains types d'avoir l'opérateur ==mais pas le !=, ou vice versa, et quand est-il logique de le faire.

En général , non, cela n'a pas de sens. L'égalité et les opérateurs relationnels se présentent généralement en ensembles. S'il y a égalité, alors l'inégalité aussi; inférieur à, puis supérieur à et ainsi de suite avec <=etc. Une approche similaire est également appliquée aux opérateurs arithmétiques, ils se présentent généralement sous forme d'ensembles logiques naturels.

Cela est démontré dans l' std::rel_opsespace de noms. Si vous implémentez l'égalité et moins d'opérateurs, l'utilisation de cet espace de noms vous donne les autres, implémentés en termes de vos opérateurs implémentés d'origine.

Cela dit, existe-t-il des conditions ou des situations dans lesquelles l'une ne signifierait pas immédiatement l'autre ou ne pourrait pas être mise en œuvre en fonction des autres? Oui, il y en a , sans doute peu, mais ils sont là; encore une fois, comme en témoigne l' rel_opsêtre un espace de noms qui lui est propre. Pour cette raison, leur mise en œuvre indépendante vous permet de tirer parti du langage pour obtenir la sémantique dont vous avez besoin ou dont vous avez besoin d'une manière toujours naturelle et intuitive pour l'utilisateur ou le client du code.

L'évaluation paresseuse déjà mentionnée en est un excellent exemple. Un autre bon exemple est de leur donner une sémantique qui ne signifie pas du tout l'égalité ou l'inégalité. Un exemple similaire à cela est les opérateurs de décalage de bits <<et >>utilisés pour l'insertion et l'extraction de flux. Bien qu'il puisse être mal vu dans les cercles généraux, dans certains domaines spécifiques, il peut avoir un sens.

Niall
la source
12

Si les opérateurs ==et !=n'impliquent pas réellement l'égalité, de la même manière que les opérateurs <<et >>stream n'impliquent pas de décalage de bits. Si vous traitez les symboles comme s'ils signifiaient un autre concept, ils ne doivent pas être mutuellement exclusifs.

En termes d'égalité, cela pourrait avoir un sens si votre cas d'utilisation justifie de traiter les objets comme non comparables, de sorte que chaque comparaison doit retourner faux (ou un type de résultat non comparable, si vos opérateurs retournent non booléen). Je ne peux pas penser à une situation spécifique où cela serait justifié, mais je pouvais voir que c'était assez raisonnable.

Taywee
la source
7

Avec une grande puissance vient très bien de manière responsable, ou du moins de très bons guides de style.

==et !=peut être surchargé pour faire tout ce que vous voulez. C'est à la fois une bénédiction et une malédiction. Il n'y a aucune garantie que cela !=signifie !(a==b).

It'sPete
la source
6
enum BoolPlus {
    kFalse = 0,
    kTrue = 1,
    kFileNotFound = -1
}

BoolPlus operator==(File& other);
BoolPlus operator!=(File& other);

Je ne peux pas justifier cette surcharge d'opérateur, mais dans l'exemple ci-dessus, il est impossible de définir operator!=comme "l'opposé" de operator==.

Dafang Cao
la source
1
@Snowman: Dafang ne dit pas que c'est une bonne énumération (ni une bonne idée de définir une énumération comme ça), c'est juste un exemple pour illustrer un point. Avec cette définition d'opérateur (peut-être mauvaise), !=cela ne signifierait donc pas le contraire de ==.
AlainD
1
@AlainD avez-vous cliqué sur le lien que j'ai publié et connaissez-vous le but de ce site? C'est ce qu'on appelle «l'humour».
1
@Snowman: Certainement ... désolé, j'ai raté que c'était un lien et destiné à l'ironie! : o)
AlainD
Attendez, vous surchargez unaire ==?
LF
5

En fin de compte, ce que vous vérifiez avec ces opérateurs, c'est que l'expression a == bou a != brenvoie une valeur booléenne ( trueou false). Cette expression renvoie une valeur booléenne après comparaison plutôt que de s'exclure mutuellement.

Anirudh Sohil
la source
4

[..] Pourquoi faut-il deux définitions distinctes?

Une chose à considérer est qu'il pourrait y avoir la possibilité de mettre en œuvre l'un de ces opérateurs plus efficacement que d'utiliser simplement la négation de l'autre.

(Mon exemple ici était des ordures, mais le point tient toujours, pensez aux filtres de floraison, par exemple: ils permettent des tests rapides si quelque chose n'est pas dans un ensemble, mais tester si c'est dans peut prendre beaucoup plus de temps.)

[..] par définition, a != best !(a == b).

Et c'est votre responsabilité en tant que programmeur de maintenir cette prise. Probablement une bonne chose pour laquelle passer un test.

Daniel Jour
la source
4
Comment ne !((a == rhs.a) && (b == rhs.b))permet pas les courts-circuits? si !(a == rhs.a), alors (b == rhs.b)ne sera pas évalué.
Benjamin Lindley
C'est un mauvais exemple, cependant. Le court-circuit n'ajoute aucun avantage magique ici.
Oliver Charlesworth
@Oliver Charlesworth Seul ce n'est pas le cas, mais lorsqu'il est associé à des opérateurs distincts, il le fait: Dans le cas de ==, il cessera de comparer dès que les premiers éléments correspondants ne seront pas égaux. Mais dans le cas où !=, s'il était implémenté en termes de ==, il faudrait d'abord comparer tous les éléments correspondants (quand ils sont tous égaux) pour pouvoir dire qu'ils ne sont pas différents: P Mais lorsqu'ils sont implémentés comme dans l'exemple ci-dessus, il cessera de comparer dès qu'il trouvera la première paire non égale. Un bel exemple en effet.
BarbaraKwarc
@BenjaminLindley C'est vrai, mon exemple était complètement absurde. Malheureusement, je ne peux pas trouver un autre guichet automatique, il est trop tard ici.
Daniel Jour
1
@BarbaraKwarc: !((a == b) && (c == d))et (a != b) || (c != d)sont équivalents en termes d'efficacité de court-circuit.
Oliver Charlesworth
2

En personnalisant le comportement des opérateurs, vous pouvez leur faire faire ce que vous voulez.

Vous voudrez peut-être personnaliser les choses. Par exemple, vous souhaiterez peut-être personnaliser une classe. Les objets de cette classe peuvent être comparés simplement en vérifiant une propriété spécifique. Sachant que c'est le cas, vous pouvez écrire du code spécifique qui ne vérifie que le minimum, au lieu de vérifier chaque bit de chaque propriété de l'objet entier.

Imaginez un cas où vous pouvez comprendre que quelque chose est différent tout aussi rapidement, sinon plus vite, que vous pouvez découvrir que quelque chose est le même. Certes, une fois que vous avez déterminé si quelque chose est identique ou différent, vous pouvez savoir le contraire simplement en retournant un peu. Cependant, retourner ce bit est une opération supplémentaire. Dans certains cas, lorsque le code est beaucoup réexécuté, l'enregistrement d'une opération (multiplié par plusieurs) peut entraîner une augmentation globale de la vitesse. (Par exemple, si vous enregistrez une opération par pixel d'un écran mégapixel, alors vous venez d'enregistrer un million d'opérations. Multiplié par 60 écrans par seconde, et vous enregistrez encore plus d'opérations.)

La réponse de hvd fournit quelques exemples supplémentaires.

TOOGAM
la source
2

Oui, car l'un signifie "équivalent" et un autre "non équivalent" et ces termes s'excluent mutuellement. Toute autre signification pour ces opérateurs prête à confusion et doit être évitée par tous les moyens.

oliora
la source
Ils ne s'excluent pas mutuellement dans tous les cas. Par exemple, deux infinis non égaux l'un à l'autre et non égaux l'un à l'autre.
vladon
@vladon peut-il utiliser l'un au lieu d'un autre dans un cas générique ? Non, cela signifie qu'ils ne sont tout simplement pas égaux. Tout le reste va à une fonction spéciale plutôt qu'à l'opérateur == /! =
oliora
@vladon s'il vous plaît, au lieu de cas générique, lisez tous les cas dans ma réponse.
oliora
@vladon Autant que cela soit vrai en mathématiques, pouvez-vous donner un exemple où a != bn'est pas égal à !(a == b)pour cette raison en C?
nitro2k01
2

Peut-être une règle incomparable, où a != bétait faux et a == bétait faux comme un bit apatride.

if( !(a == b || a != b) ){
    // Stateless
}
ToñitoG
la source
Si vous souhaitez réorganiser les symboles logiques, alors! ([A] || [B]) devient logiquement ([! A] & [! B])
Thijser
Notez que le type de retour de operator==()etoperator!=() ne le sont pas nécessairement bool, ils peuvent être une énumération qui inclut les apatrides si vous le souhaitez et pourtant les opérateurs peuvent toujours être définis, donc c'est vrai (a != b) == !(a==b).
lorro