J'apprends la surcharge d'opérateurs en C++ et je vois que ==
et !=
sont simplement des fonctions spéciales qui peuvent être personnalisées pour des types définis par l'utilisateur. Ce qui me préoccupe, cependant, est de savoir pourquoi deux définitions distinctes sont-elles nécessaires? Je pensais que si a == b
est vrai, alors a != b
est automatiquement faux, et inversement, et il n'y a pas d'autre possibilité, car, par définition, a != b
est !(a == b)
. Et je ne pouvais imaginer aucune situation dans laquelle ce ne serait 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 pose pas non plus de question sur la distinction entre la comparaison d'objets par valeur ou par identité. Ou si deux objets peuvent être égaux et non égaux en même temps (ce n'est certainement pas une option! Ces choses sont mutuellement exclusives). Ce que je demande est la suivante:
Y a-t-il une situation possible dans laquelle poser des questions sur l'équivalence de deux objets n'a pas de sens, mais poser des questions à leur sujet et non ne pas avoir de sens? (soit du point de vue de l'utilisateur, ou du point de vue de l'implémenteur)
S'il n'y a pas une telle possibilité, alors pourquoi sur C++, ces deux opérateurs sont-ils définis comme deux fonctions distinctes?
Vous voudriez pas que la langue réécrive automatiquement a != b
sous la forme !(a == b)
lorsque a == b
renvoie autre chose qu'un bool
. Et il y a plusieurs raisons pour lesquelles vous pourriez le faire faire.
Vous pouvez avoir des objets de générateur d’expression, où a == b
n’a pas et n’est pas destiné à effectuer une comparaison, mais construit simplement un noeud d’expression représentant a == b
.
Vous pouvez avoir une évaluation paresseuse, où a == b
ne permet pas et n’est pas destiné à effectuer directement une comparaison, mais renvoie plutôt une sorte de lazy<bool>
pouvant être converti en bool
implicitement ou explicitement. à un moment ultérieur pour effectuer la comparaison. Peut-être combiné avec les objets du générateur d'expression pour permettre l'optimisation complète de l'expression avant l'évaluation.
Vous pouvez avoir une classe de modèles optional<T>
personnalisée, où vous souhaitez autoriser t == u
à l'aide des variables facultatives t
et u
, tout en le renvoyant optional<bool>
.
Il y a probablement plus que je n'ai pas pensé. Et même si, dans ces exemples, l'opération a == b
et a != b
ont un sens, toujours a != b
n'est pas la même chose que !(a == b)
, des définitions séparées sont donc nécessaires.
S'il n'y a pas une telle possibilité, alors pourquoi sur C++, ces deux opérateurs sont-ils définis comme deux fonctions distinctes?
Parce que vous pouvez les surcharger, et en les surchargeant, vous pouvez leur donner une signification totalement différente de celle d'origine.
Prenons, par exemple, l'opérateur <<
, qui était à l'origine l'opérateur de décalage gauche au niveau du bit, désormais 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 une signification à l'opérateur ==
qui n'est pas exactement le négation de opérateur !=
, bien que cela puisse être déroutant.
Ce qui me préoccupe, cependant, c'est pourquoi deux définitions distinctes sont-elles nécessaires?
Vous n'êtes pas obligé de définir les deux.
S'ils s'excluent mutuellement, vous pouvez toujours être concis en définissant uniquement ==
et <
à côté de std :: rel_ops
De référence:
#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 l'équivalence de deux objets a du sens, mais poser des questions sur le fait que l'égalité n'est pas égale n'a pas de sens?
Nous associons souvent ces opérateurs à l'égalité.
Bien que ce soit la manière dont ils se comportent vis-à-vis des types fondamentaux, rien ne l’oblige à agir de la sorte sur les types de données personnalisés. Vous n'êtes même pas obligé de retourner un bool si vous ne voulez pas.
J'ai vu des personnes surcharger les opérateurs de manière bizarre, mais je me suis rendu compte que cela était logique pour leur application spécifique à un 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, ou du point de vue de l'implémenteur)
Je sais que vous voulez un exemple spécifique,
donc voici un extrait du cadre de test des captures que j’ai trouvé 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 une! (Pas) de l’autre. La raison en est que le cadre peut imprimer la comparaison. Pour ce faire, il doit capturer le contexte de l’opérateur surchargé utilisé.
Il existe des conventions très bien établies dans lesquelles (a == b)
et (a != b)
sont à la fois faux pas nécessairement des contraires. En particulier, en SQL, toute comparaison avec NULL donne NULL, ni vrai ni faux.
Ce n'est probablement pas une bonne idée de créer de nouveaux exemples si possible, parce que c'est tellement peu intuitif, mais si vous essayez de modéliser une convention existante, il est agréable d'avoir l'option de faire en sorte que vos opérateurs se comportent "correctement". le contexte.
Je ne répondrai qu'à la deuxième partie de votre question, à savoir:
S'il n'y a pas une telle possibilité, alors pourquoi sur C++, ces deux opérateurs sont-ils 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 autoriser des optimisations en implémentant à la fois ==
et !=
. Alors, x != y
pourrait être meilleur marché que !(x == y)
. Certains compilateurs pourront peut-être l'optimiser pour vous, mais peut-être pas, surtout si vous avez des objets complexes impliquant beaucoup de ramifications.
Même en Haskell, où les développeurs prennent les lois et les concepts mathématiques très au sérieux, on est toujours autorisé à surcharger ==
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.
Y a-t-il une situation possible dans laquelle poser des questions sur l'équivalence de deux objets a du sens, mais poser des questions sur le fait que l'égalité n'est pas égale n'a pas de sens? (du point de vue de l'utilisateur ou du réalisateur)
C'est un avis. Peut-être que non. Mais les concepteurs de langage, n’étant pas omniscients, ont décidé de ne pas restreindre les personnes qui pourraient se trouver dans des situations où cela pourrait avoir un sens (du moins pour eux).
En réponse à la modification;
En d’autres termes, s’il est possible qu’un type ait l’opérateur _
==
_ mais pas le _!=
_, ou l’inverse, et quand est-il judicieux de le faire.
Dans général , non, cela n'a pas de sens. Les opérateurs d’égalité et relationnels viennent généralement par 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, qui se présentent généralement également dans des ensembles logiques naturels.
Ceci est démontré dans l'espace de noms std::rel_ops
. Si vous implémentez les opérateurs d'égalité et inférieur à, l'utilisation de cet espace de noms vous en donne les autres, implémentés en termes d'opérateurs implémentés d'origine.
Cela dit, existe-t-il des conditions ou des situations où l’une ne signifie pas immédiatement l’autre, ou ne peut pas être mise en œuvre par rapport aux autres? Oui, il y en a , sans doute peu, mais ils sont là; encore une fois, comme en témoigne le _rel_ops
_ étant un espace de noms propre. Pour cette raison, autoriser leur implémentation de manière indépendante vous permet d'exploiter le langage pour obtenir la sémantique dont vous avez besoin ou dont vous avez besoin de manière 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 veut pas dire égalité ou inégalité. Un exemple similaire à cela est celui des opérateurs de décalage de bits _<<
_ et _>>
_ utilisés pour l'insertion et l'extraction de flux. Bien que cela puisse être mal vu dans les cercles généraux, dans certains domaines spécifiques, cela peut avoir un sens.
Si les opérateurs ==
et !=
n'impliquent pas réellement l'égalité, de la même manière que les opérateurs de flux <<
et >>
n'impliquent pas le transfert de bits. Si vous traitez les symboles comme s’ils signifiaient un autre concept, ils ne devaient pas s’exclure mutuellement.
En termes d'égalité, il peut être judicieux que votre cas d'utilisation justifie de traiter les objets comme non comparables, de sorte que chaque comparaison renvoie un résultat faux (ou un type de résultat non comparable si vos opérateurs renvoient une valeur non booléenne). Je ne peux pas penser à une situation spécifique où cela serait justifié, mais je pouvais le voir suffisamment raisonnable.
Une grande puissance est synonyme de responsabilité, ou tout au moins de très bons guides de style.
==
et !=
peuvent être surchargés pour faire tout ce que vous voulez. C'est à la fois une bénédiction et une malédiction. Rien ne garantit que !=
signifie !(a==b)
.
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==
.
En fin de compte, ce que vous vérifiez avec ces opérateurs, c'est que l'expression a == b
ou a != b
renvoie une valeur booléenne (true
ou false
). Ces expressions renvoient une valeur booléenne après comparaison plutôt que d'être mutuellement exclusives.
[..] pourquoi deux définitions distinctes sont-elles nécessaires?
Une chose à considérer est qu’il pourrait être possible de mettre en œuvre l’un de ces opérateurs plus efficacement que de simplement utiliser la négation de l’autre.
(Mon exemple ici était de la foutaise, mais le problème est toujours valable, pensez aux filtres de bloom, par exemple: ils permettent un test rapide si quelque chose est non dans un ensemble, mais le test si cela prend peut prendre beaucoup plus temps.)
[..] par définition,
a != b
est!(a == b)
.
Et c'est votre responsabilité en tant que programmeur de le faire. Probablement une bonne chose pour écrire un test.
Peut-être une règle incomparable, où a != b
était false et a == b
était false comme un bit sans état.
if( !(a == b || a != b) ){
// Stateless
}
Oui, car l'un signifie "équivalent" et un autre "non-équivalent" et ces termes sont mutuellement exclusifs. Toute autre signification pour cet opérateur est source de confusion et doit être évitée par tous les moyens.
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 un 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 aussi rapidement, sinon plus, que vous pouvez découvrir que quelque chose est identique. 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é, la sauvegarde d’une opération (multipliée 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, vous venez d'enregistrer un million d'opérations. Multiplié par 60 écrans par seconde et vous enregistrez encore plus d'opérations.)
Réponse du DVD fournit quelques exemples supplémentaires.