Pourquoi les compilateurs C ++ ne définissent-ils pas l'opérateur == et l'opérateur! =?

302

Je suis un grand fan de laisser le compilateur faire autant de travail que possible pour vous. Lors de l'écriture d'une classe simple, le compilateur peut vous donner les éléments suivants gratuitement:

  • Un constructeur par défaut (vide)
  • Un constructeur de copie
  • Un destructeur
  • Un opérateur d'affectation ( operator=)

Mais il ne semble pas vous donner d'opérateurs de comparaison - comme operator==ou operator!=. Par exemple:

class foo
{
public:
    std::string str_;
    int n_;
};

foo f1;        // Works
foo f2(f1);    // Works
foo f3;
f3 = f2;       // Works

if (f3 == f2)  // Fails
{ }

if (f3 != f2)  // Fails
{ }

Y a-t-il une bonne raison à cela? Pourquoi effectuer une comparaison membre par membre serait un problème? Évidemment, si la classe alloue de la mémoire, vous voudrez être prudent, mais pour une classe simple, le compilateur pourrait sûrement le faire pour vous?

Rob
la source
4
Bien sûr, le destructeur est également fourni gratuitement.
Johann Gerell
23
Dans l'un de ses récents entretiens, Alex Stepanov a souligné que c'était une erreur de ne pas avoir une automatique par défaut ==, de la même manière qu'il y a une affectation automatique par défaut ( =) sous certaines conditions. (L'argument sur les pointeurs est incohérent car la logique s'applique à la fois pour =et ==, et pas seulement pour le second).
alfC
2
@becko C'est l'un de la série sur A9: youtube.com/watch?v=k-meLQaYP5Y , je ne me souviens pas dans laquelle des discussions. Il y a aussi une proposition qui semble se diriger vers le C ++ 17 open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0221r0.html
alfC
1
@becko, il est l'un des premiers de la série "Programmation efficace avec composants" ou "Programmation des conversations" tous deux sur A9, disponible sur Youtube.
alfC
1
@becko En fait, il y a une réponse ci-dessous pointant le point de vue d'Alex stackoverflow.com/a/23329089/225186
alfC

Réponses:

71

Le compilateur ne saurait pas si vous vouliez une comparaison de pointeurs ou une comparaison approfondie (interne).

Il est plus sûr de ne pas l'implémenter et de laisser le programmeur le faire lui-même. Ensuite, ils peuvent faire toutes les hypothèses qu'ils aiment.

Mark Ingram
la source
293
Ce problème ne l'empêche pas de générer un ctor de copie, où il est assez dangereux.
MSalters
78
Les constructeurs de copie (et operator=) fonctionnent généralement dans le même contexte que les opérateurs de comparaison - c'est-à-dire que l'on s'attend à ce que, après avoir effectué a = b, cela a == bsoit vrai. Il est certainement logique que le compilateur fournisse une valeur operator==par défaut en utilisant la même sémantique de valeur agrégée que pour operator=. Je soupçonne que paercebal a en fait raison ici en ce que operator=(et copie ctor) sont fournis uniquement pour la compatibilité C, et ils ne voulaient pas aggraver la situation.
Pavel Minaev,
46
-1. Bien sûr, vous voulez une comparaison approfondie, si le programmeur voulait une comparaison de pointeurs, il écrirait (& f1 == & f2)
Viktor Sehr
62
Viktor, je vous suggère de repenser votre réponse. Si la classe Foo contient un Bar *, alors comment le compilateur pourrait-il savoir si Foo :: operator == veut comparer l'adresse de Bar * ou le contenu de Bar?
Mark Ingram
46
@Mark: S'il contient un pointeur, la comparaison des valeurs du pointeur est raisonnable - s'il contient une valeur, la comparaison des valeurs est raisonnable. Dans des circonstances exceptionnelles, le programmeur pourrait passer outre. C'est tout comme le langage implémente la comparaison entre les entiers et les pointeurs vers les entiers.
Eamon Nerbonne,
317

L'argument selon lequel si le compilateur peut fournir un constructeur de copie par défaut, il devrait être en mesure de fournir un défaut similaire a operator==()un certain sens. Je pense que la raison de la décision de ne pas fournir un défaut généré par le compilateur pour cet opérateur peut être devinée par ce que Stroustrup a dit à propos du constructeur de copie par défaut dans "La conception et l'évolution de C ++" (Section 11.4.1 - Contrôle de la copie) :

Personnellement, je trouve regrettable que les opérations de copie soient définies par défaut et j'interdise la copie d'objets de plusieurs de mes classes. Cependant, C ++ a hérité de ses assignations par défaut et des constructeurs de copie de C, et ils sont fréquemment utilisés.

Donc, au lieu de "pourquoi C ++ n'a-t-il pas de valeur par défaut operator==() ?", La question aurait dû être "pourquoi C ++ a-t-il un constructeur d'affectation et de copie par défaut?", La réponse étant que ces éléments ont été inclus à contrecœur par Stroustrup pour une compatibilité descendante avec C (probablement la cause de la plupart des verrues de C ++, mais aussi probablement la principale raison de la popularité de C ++).

À mes propres fins, dans mon IDE, l'extrait de code que j'utilise pour les nouvelles classes contient des déclarations pour un opérateur d'affectation privé et un constructeur de copie, de sorte que lorsque je crée une nouvelle classe, je n'obtiens aucune affectation par défaut et aucune opération de copie - je dois supprimer explicitement la déclaration de ces opérations dans la private:section si je veux que le compilateur puisse les générer pour moi.

Michael Burr
la source
30
Bonne réponse. Je voudrais juste souligner qu'en C ++ 11, plutôt que de rendre l'opérateur d'affectation et le constructeur de copie privés, vous pouvez les supprimer complètement comme ceci: Foo(const Foo&) = delete; // no copy constructoretFoo& Foo=(const Foo&) = delete; // no assignment operator
karadoc
9
"Cependant, C ++ a hérité de ses assignations par défaut et des constructeurs de copie de C". Cela n'implique pas pourquoi vous devez créer TOUS les types C ++ de cette façon. Ils auraient dû juste restreindre cela aux anciens POD simples, juste aux types qui sont déjà en C, pas plus.
thesaint
3
Je peux certainement comprendre pourquoi le C ++ a hérité de ces comportements struct, mais je souhaite qu'il laisse classse comporter différemment (et sainement). Dans le processus, cela aurait également donné une différence plus significative entre structet à classcôté de l'accès par défaut.
jamesdlin
@jamesdlin Si vous voulez une règle, désactiver la déclaration implicite et la définition des ctors et l'affectation si un dtor est déclaré aurait le plus de sens.
Déduplicateur
1
Je ne vois toujours aucun mal à laisser le programmeur ordonner explicitement au compilateur de créer un fichier operator==. À ce stade, il ne s'agit que de sucre de syntaxe pour un code de plaque de chaudière. Si vous craignez que de cette façon le programmeur puisse ignorer un pointeur parmi les champs de classe, vous pouvez ajouter une condition selon laquelle il ne peut fonctionner que sur les types primitifs et les objets qui ont eux-mêmes des opérateurs d'égalité. Cependant, il n'y a aucune raison de rejeter entièrement cela.
NO_NAME
93

Même en C ++ 20, le compilateur ne génère toujours pas implicitement operator==pour vous

struct foo
{
    std::string str;
    int n;
};

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ill-formed

Mais vous aurez la possibilité de explicitement par défaut == depuis C ++ 20 :

struct foo
{
    std::string str;
    int n;

    // either member form
    bool operator==(foo const&) const = default;
    // ... or friend form
    friend bool operator==(foo const&, foo const&) = default;
};

Défaillant ==fait sage membre ==(de la même manière que le constructeur de copie par défaut ne construction de copie-sage membre). Les nouvelles règles fournissent également la relation attendue entre ==et !=. Par exemple, avec la déclaration ci-dessus, je peux écrire les deux:

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ok!
assert(foo{"Anton", 1} != foo{"Anton", 2}); // ok!

Cette fonctionnalité spécifique (défaut operator==et symétrie entre ==et !=) provient d' une proposition qui faisait partie de la fonctionnalité de langage plus large qui est operator<=>.

Anton Savin
la source
Savez-vous s'il existe une mise à jour plus récente à ce sujet? Va-t-il être disponible en c ++ 17?
dcmm88
3
@ dcmm88 Malheureusement, il ne sera pas disponible en C ++ 17. J'ai mis à jour la réponse.
Anton Savin
2
Une proposition modifiée qui autorise la même chose (sauf la forme abrégée) sera cependant en C ++ 20 :)
Rakete1111
Donc, fondamentalement, vous devez spécifier = default, pour une chose qui n'est pas créée par défaut, non? Cela ressemble à de l'oxymore pour moi ("défaut explicite").
artin
@artin Cela a du sens car l'ajout de nouvelles fonctionnalités au langage ne doit pas casser l'implémentation existante. Ajouter de nouvelles normes de bibliothèque ou de nouvelles choses que le compilateur peut faire est une chose. L'ajout de nouvelles fonctions membres là où elles n'existaient pas auparavant est une toute autre histoire. Pour protéger votre projet contre les erreurs, il faudrait beaucoup plus d'efforts. Personnellement, je préférerais que l'indicateur du compilateur bascule entre la valeur implicite et explicite. Vous créez un projet à partir d'une ancienne norme C ++, utilisez un indicateur explicite par défaut du compilateur. Vous mettez déjà à jour le compilateur, vous devez donc le configurer correctement. Pour les nouveaux projets, le rendre implicite.
Maciej Załucki
44

À mon humble avis, il n'y a pas de "bonne" raison. La raison pour laquelle tant de gens sont d'accord avec cette décision de conception est qu'ils n'ont pas appris à maîtriser la puissance de la sémantique basée sur les valeurs. Les gens ont besoin d'écrire beaucoup de constructeur de copie personnalisé, d'opérateurs de comparaison et de destructeurs car ils utilisent des pointeurs bruts dans leur implémentation.

Lorsque vous utilisez des pointeurs intelligents appropriés (comme std :: shared_ptr), le constructeur de copie par défaut est généralement correct et l'implémentation évidente de l'opérateur de comparaison par défaut hypothétique est aussi fine.

alexk7
la source
39

Il est répondu que C ++ n'a pas fait == parce que C n'a pas fait, et voici pourquoi C fournit uniquement default = mais pas == à la première place. C voulait rester simple: C implémenté = par memcpy; cependant, == ne peut pas être implémenté par memcmp en raison du remplissage. Parce que le remplissage n'est pas initialisé, memcmp dit qu'ils sont différents même s'ils sont identiques. Le même problème existe pour les classes vides: memcmp dit qu'elles sont différentes car la taille des classes vides n'est pas nulle. On peut voir d'en haut que l'implémentation de == est plus compliquée que l'implémentation de = en C. Quelques exemples de code à ce sujet. Votre correction est appréciée si je me trompe.

Aile de Rio
la source
6
C ++ n'utilise pas memcpy pour operator=- cela ne fonctionnerait que pour les types POD, mais C ++ fournit également une valeur operator=par défaut pour les types non POD.
Flexo
2
Oui, C ++ implémenté = d'une manière plus sophistiquée. Il semble que C vient d'être implémenté = avec un simple memcpy.
Rio Wing
Le contenu de cette réponse doit être associé à celui de Michael. Son corrige la question puis cela y répond.
Sgene9
27

Dans cette vidéo Alex Stepanov, le créateur de STL répond à cette même question vers 13h00. Pour résumer, après avoir observé l'évolution du C ++, il soutient que:

  • Il est regrettable que == et! = Ne soient pas implicitement déclarés (et Bjarne est d'accord avec lui). Un langage correct devrait avoir ces choses à portée de main (il va plus loin pour suggérer que vous ne devriez pas être en mesure de définir un ! = Qui rompt la sémantique de == )
  • La raison pour laquelle c'est le cas a ses racines (comme beaucoup de problèmes C ++) en C. Là, l'opérateur d'affectation est implicitement défini avec une affectation bit par bit mais cela ne fonctionnerait pas pour == . Une explication plus détaillée peut être trouvée dans cet article de Bjarne Stroustrup.
  • Dans la question de suivi Pourquoi alors une comparaison membre par membre n'a-t-elle pas été utilisée, il dit une chose incroyable : C était une sorte de langage local et le gars qui implémentait ces trucs pour Ritchie lui a dit qu'il trouvait cela difficile à mettre en œuvre!

Il dit ensuite que dans un avenir (lointain) == et ! = Seront implicitement générés.

Nikos Athanasiou
la source
2
semble que cet avenir lointain ne va pas être 2017 ni 18, ni 19, eh bien vous attrapez ma dérive ...
UmNyobe
18

C ++ 20 permet d'implémenter facilement un opérateur de comparaison par défaut.

Exemple de cppreference.com :

class Point {
    int x;
    int y;
public:
    auto operator<=>(const Point&) const = default;
    // ... non-comparison functions ...
};

// compiler implicitly declares operator== and all four relational operators work
Point pt1, pt2;
if (pt1 == pt2) { /*...*/ } // ok, calls implicit Point::operator==
std::set<Point> s; // ok
s.insert(pt1); // ok
if (pt1 <= pt2) { /*...*/ } // ok, makes only a single call to Point::operator<=>
vll
la source
4
Je suis surpris qu'ils aient utilisé Pointcomme exemple pour une opération de commande , car il n'y a pas de moyen par défaut raisonnable pour commander deux points avec xet ycoordonnées ...
pipe
4
@pipe Si vous ne vous souciez pas de l'ordre des éléments, utiliser l'opérateur par défaut est logique. Par exemple, vous pouvez utiliser std::setpour vous assurer que tous les points sont uniques et std::setutilisent operator<uniquement.
vll
À propos du type de retour auto: Dans ce cas, pouvons-nous toujours supposer qu'il proviendra std::strong_orderingde #include <compare>?
kevinarpe
1
@kevinarpe Le type de retour est std::common_comparison_category_t, qui pour cette classe devient le classement par défaut ( std::strong_ordering).
vll
15

Il n'est pas possible de définir la valeur par défaut ==, mais vous pouvez définir la valeur !=par défaut via ==laquelle vous devez généralement vous définir. Pour cela, vous devez faire les choses suivantes:

#include <utility>
using namespace std::rel_ops;
...

class FooClass
{
public:
  bool operator== (const FooClass& other) const {
  // ...
  }
};

Vous pouvez consulter http://www.cplusplus.com/reference/std/utility/rel_ops/ pour plus de détails.

De plus, si vous définissez operator< , les opérateurs pour <=,>,> = peuvent en être déduits lors de l'utilisation std::rel_ops.

Mais vous devez être prudent lorsque vous utilisez std::rel_opscar les opérateurs de comparaison peuvent être déduits pour les types auxquels vous n'êtes pas attendu.

La manière la plus préférée de déduire un opérateur lié d'un opérateur de base est d'utiliser boost :: operators .

L'approche utilisée dans boost est meilleure car elle définit l'utilisation de l'opérateur pour la classe que vous souhaitez uniquement, pas pour toutes les classes de la portée.

Vous pouvez également générer "+" à partir de "+ =", - à partir de "- =", etc ... (voir la liste complète ici )

sergtk
la source
Je n'ai pas eu de défaut !=après avoir écrit l' ==opérateur. Ou je l'ai fait, mais cela manquait const. J'ai dû l'écrire moi aussi et tout allait bien.
John
vous pouvez jouer avec constance pour obtenir les résultats nécessaires. Sans code, il est difficile de dire ce qui ne va pas.
sergtk
2
Il y a une raison rel_opsdéconseillée en C ++ 20: parce que cela ne fonctionne pas , du moins pas partout, et certainement pas de manière cohérente. Il n'y a aucun moyen fiable sort_decreasing()de compiler. D'un autre côté, Boost.Operators fonctionne et a toujours fonctionné.
Barry
10

C ++ 0x a eu une proposition de fonctions par défaut, vous pouvez donc dire que default operator==; nous avons appris que cela aide à rendre ces choses explicites.

MSalters
la source
3
Je pensais que seules les "fonctions membres spéciales" (constructeur par défaut, constructeur de copie, opérateur d'assignation et destructeur) pouvaient être explicitement définies par défaut. Ont-ils étendu cela à d'autres opérateurs?
Michael Burr
4
Le constructeur de déplacement peut également être par défaut, mais je ne pense pas que cela s'applique à operator==. C'est dommage.
Pavel Minaev,
5

Conceptuellement, il n'est pas facile de définir l'égalité. Même pour les données POD, on pourrait faire valoir que même si les champs sont les mêmes, mais qu'il s'agit d'un objet différent (à une adresse différente), il n'est pas nécessairement égal. Cela dépend en fait de l'utilisation de l'opérateur. Malheureusement, votre compilateur n'est pas psychique et ne peut pas en déduire.

En plus de cela, les fonctions par défaut sont d'excellents moyens de se tirer une balle dans le pied. Les valeurs par défaut que vous décrivez sont essentiellement là pour maintenir la compatibilité avec les structures POD. Cependant, ils causent plus que suffisamment de ravages, les développeurs les oubliant ou la sémantique des implémentations par défaut.

Paul de Vrieze
la source
10
Il n'y a aucune ambiguïté pour les structures POD - elles devraient se comporter exactement de la même manière que tout autre type de POD, ce qui est l'égalité de valeur (plutôt que l'égalité de référence). Celui intcréé via la copie ctor d'un autre est égal à celui à partir duquel il a été créé; la seule chose logique à faire pour l'un structdes deux intdomaines est de travailler exactement de la même manière.
Pavel Minaev,
1
@mgiuca: Je peux voir une utilité considérable pour une relation d'équivalence universelle qui permettrait à tout type qui se comporte comme une valeur d'être utilisé comme clé dans un dictionnaire ou une collection similaire. Cependant, de telles collections ne peuvent pas se comporter utilement sans une relation d'équivalence garantie-réflexive. À mon humble avis, la meilleure solution serait de définir un nouvel opérateur que tous les types intégrés pourraient implémenter de manière sensée, et de définir de nouveaux types de pointeurs qui ressemblaient aux modèles existants, sauf que certains définiraient l'égalité comme équivalence de référence tandis que d'autres enchaîneraient à la cible. opérateur d'équivalence.
supercat
1
@supercat Par analogie, vous pourriez faire presque le même argument pour l' +opérateur en ce qu'il n'est pas associatif pour les flottants; c'est-à-dire (x + y) + z! = x + (y + z), en raison de la façon dont l'arrondi FP se produit. (Il s'agit sans doute d'un problème bien pire que ==parce qu'il est vrai pour les valeurs numériques normales.) Vous pouvez suggérer d'ajouter un nouvel opérateur d'addition qui fonctionne pour tous les types numériques (même int) et qui est presque exactement le même +mais associatif ( en quelque sorte). Mais alors, vous ajouteriez des ballonnements et de la confusion à la langue sans vraiment aider autant de gens.
mgiuca
1
@mgiuca: Avoir des choses assez similaires, sauf dans les cas limites, est souvent extrêmement utile, et les efforts malavisés pour éviter de telles choses entraînent une complexité inutile. Si le code client a parfois besoin que les cas marginaux soient traités dans un sens, et parfois qu'il en soit besoin d'une autre, avoir une méthode pour chaque style de gestion éliminera beaucoup de code de gestion des cas marginaux dans le client. Quant à votre analogie, il n'y a aucun moyen de définir le fonctionnement sur des valeurs à virgule flottante de taille fixe pour donner des résultats transitifs dans tous les cas (bien que certaines langues des années 1980 aient une meilleure sémantique ...
supercat
1
... que d'aujourd'hui à cet égard) et donc le fait qu'ils ne font pas l'impossible ne devrait pas être une surprise. Il n'y a cependant pas d'obstacle fondamental à la mise en œuvre d'une relation d'équivalence qui serait universellement applicable à tout type de valeur pouvant être copiée.
supercat
1

Y a-t-il une bonne raison à cela? Pourquoi effectuer une comparaison membre par membre serait un problème?

Ce n'est peut-être pas un problème sur le plan fonctionnel, mais en termes de performances, la comparaison membre par membre par défaut est susceptible d'être plus sous-optimale que l'attribution / copie membre par membre par défaut. Contrairement à l'ordre d'affectation, l'ordre de comparaison a un impact sur les performances car le premier membre inégal implique que le reste peut être ignoré. Donc, s'il y a des membres qui sont généralement égaux, vous voulez les comparer en dernier, et le compilateur ne sait pas quels membres sont plus susceptibles d'être égaux.

Considérez cet exemple, où verboseDescriptionest une longue chaîne sélectionnée parmi un ensemble relativement petit de descriptions météorologiques possibles.

class LocalWeatherRecord {
    std::string verboseDescription;
    std::tm date;
    bool operator==(const LocalWeatherRecord& other){
        return date==other.date
            && verboseDescription==other.verboseDescription;
    // The above makes a lot more sense than
     // return verboseDescription==other.verboseDescription
     //     && date==other.date;
    // because some verboseDescriptions are liable to be same/similar
    }
}

(Bien sûr, le compilateur aurait le droit de ne pas tenir compte de l'ordre des comparaisons s'il reconnaît qu'il n'y a pas d'effets secondaires, mais il est probable qu'il tirera toujours son nom du code source où il ne dispose pas de meilleures informations.)

Museful
la source
Mais personne ne vous empêche d'écrire une comparaison optimisée définie par l'utilisateur si vous rencontrez un problème de performances. D'après mon expérience, ce ne serait qu'une infime minorité de cas.
Peter - Réintègre Monica
1

Juste pour que les réponses à cette question restent complètes au fil du temps: depuis C ++ 20 il peut être généré automatiquement avec la commande auto operator<=>(const foo&) const = default;

Il générera tous les opérateurs: ==,! =, <, <=,> Et> =, voir https://en.cppreference.com/w/cpp/language/default_comparisons pour plus de détails.

En raison de l'apparence de l'opérateur <=>, il est appelé opérateur de vaisseau spatial. Voir aussi Pourquoi avons-nous besoin de l'opérateur <=> du vaisseau spatial en C ++? .

EDIT: également en C ++ 11 un substitut assez soigné qui est disponible avec std::tievoir https://en.cppreference.com/w/cpp/utility/tuple/tie pour un exemple de code complet avec bool operator<(…). La partie intéressante, modifiée pour fonctionner avec ==est:

#include <tuple>

struct S {
………
bool operator==(const S& rhs) const
    {
        // compares n to rhs.n,
        // then s to rhs.s,
        // then d to rhs.d
        return std::tie(n, s, d) == std::tie(rhs.n, rhs.s, rhs.d);
    }
};

std::tie fonctionne avec tous les opérateurs de comparaison et est complètement optimisé par le compilateur.

cosurgi
la source
-1

Je suis d'accord, pour les classes de type POD, le compilateur pourrait le faire pour vous. Cependant, ce que vous pourriez considérer comme simple, le compilateur peut se tromper. Il est donc préférable de laisser le programmeur le faire.

J'ai eu un cas POD une fois où deux des champs étaient uniques - donc une comparaison ne serait jamais considérée comme vraie. Cependant, la comparaison dont j'avais besoin n'a jamais été comparée que sur la charge utile - quelque chose que le compilateur ne comprendrait jamais ou ne pourrait jamais comprendre par lui-même.

D'ailleurs - ils ne tardent pas à écrire, n'est-ce pas?!

graham.reeds
la source