web-dev-qa-db-fra.com

Quelle est la bonne façon de surcharger l'opérateur == pour une hiérarchie de classes?

Supposons que j'ai la hiérarchie de classe suivante:

class A
{
    int foo;
    virtual ~A() = 0;
};

A::~A() {}

class B : public A
{
    int bar;
};

class C : public A
{
    int baz;
};

Quelle est la bonne façon de surcharger operator== pour ces classes? Si je leur donne toutes des fonctions libres, alors B et C ne pourront pas tirer parti de la version de A sans casting. Cela empêcherait également quelqu'un de faire une comparaison en profondeur en ne se référant qu'à A. Si je les convertis en fonctions de membre virtuel, une version dérivée pourrait ressembler à ceci:

bool B::operator==(const A& rhs) const
{
    const B* ptr = dynamic_cast<const B*>(&rhs);        
    if (ptr != 0) {
        return (bar == ptr->bar) && (A::operator==(*this, rhs));
    }
    else {
        return false;
    }
}

Encore une fois, je dois encore lancer (et ça ne va pas). Y a-t-il un moyen préféré de faire cela?

Mettre à jour:

Jusqu'à présent, il n'y a que deux réponses, mais il semble que la bonne manière est analogue à l'opérateur d'affectation:

  • Faire un résumé des classes non-feuille
  • Protégé non virtuel dans les classes non-feuille
  • Public non virtuel dans les classes feuille

Toute tentative de comparaison de deux objets de types différents par l'utilisateur ne sera pas compilée, car la fonction de base est protégée et les classes feuille peuvent exploiter la version du parent pour comparer cette partie des données.

35

Pour ce type de hiérarchie, je suivrais certainement le conseil Effective C++ de Scott Meyer et évitais d'avoir des classes de base concrètes. Vous semblez le faire dans tous les cas.

Je mettrais en œuvre operator== en tant que fonctions libres, probablement des amis, uniquement pour les types de classe concrets nœud feuille.

Si la classe de base doit avoir des données membres, je fournirais alors une fonction d'assistance non virtuelle (probablement protégée) dans la classe de base (isEqual, par exemple) que le operator== des classes dérivées pourrait utiliser.

Par exemple.

bool operator==(const B& lhs, const B& rhs)
{
    lhs.isEqual( rhs ) && lhs.bar == rhs.bar;
}

En évitant d'avoir un operator== qui fonctionne sur des classes de base abstraites et en gardant les fonctions de comparaison protégées, vous n'obtenez jamais de repli accidentel dans le code client où seule la partie de base de deux objets de types différents est comparée.

Je ne sais pas si j'implémenterais une fonction de comparaison virtuelle avec un dynamic_cast. Je serais réticent à le faire, mais si le besoin s'en faisait sentir, j'utiliserais probablement une fonction virtuelle pure dans la classe de base ( not operator==) qui a ensuite été remplacé dans les classes dérivées concrètes par quelque chose comme ceci, en utilisant le operator== pour la classe dérivée.

bool B::pubIsEqual( const A& rhs ) const
{
    const B* b = dynamic_cast< const B* >( &rhs );
    return b != NULL && *this == *b;
}
13
CB Bailey

J'avais le même problème l'autre jour et j'ai proposé la solution suivante:

struct A
{
    int foo;
    A(int prop) : foo(prop) {}
    virtual ~A() {}
    virtual bool operator==(const A& other) const
    {
        if (typeid(*this) != typeid(other))
            return false;

        return foo == other.foo;
    }
};

struct B : A
{
    int bar;
    B(int prop) : A(1), bar(prop) {}
    bool operator==(const A& other) const
    {
        if (!A::operator==(other))
            return false;

        return bar == static_cast<const B&>(other).bar;
    }
};

struct C : A
{
    int baz;
    C(int prop) : A(1), baz(prop) {}
    bool operator==(const A& other) const
    {
        if (!A::operator==(other))
            return false;

        return baz == static_cast<const C&>(other).baz;
    }
};

La chose que je n'aime pas à ce sujet est la vérification de typeid. Qu'est-ce que tu en penses?

12
Job

Si vous faites l'hypothèse raisonnable que les types des deux objets doivent être identiques pour qu'ils soient égaux, il existe un moyen de réduire la quantité de plaque de chaudière requise dans chaque classe dérivée. Ceci fait suite à la recommandation de Herb Sutter de garder les méthodes virtuelles protégées et cachées derrière une interface publique. Le modèle de modèle curieusement récurrent (CRTP) est utilisé pour implémenter le code général dans la méthode equals afin que les classes dérivées n'en aient pas besoin.

class A
{
public:
    bool operator==(const A& a) const
    {
        return equals(a);
    }
protected:
    virtual bool equals(const A& a) const = 0;
};

template<class T>
class A_ : public A
{
protected:
    virtual bool equals(const A& a) const
    {
        const T* other = dynamic_cast<const T*>(&a);
        return other != nullptr && static_cast<const T&>(*this) == *other;
    }
private:
    bool operator==(const A_& a) const  // force derived classes to implement their own operator==
    {
        return false;
    }
};

class B : public A_<B>
{
public:
    B(int i) : id(i) {}
    bool operator==(const B& other) const
    {
        return id == other.id;
    }
private:
    int id;
};

class C : public A_<C>
{
public:
    C(int i) : identity(i) {}
    bool operator==(const C& other) const
    {
        return identity == other.identity;
    }
private:
    int identity;
};

Voir une démo sur http://ideone.com/SymduV

9
Mark Ransom

Si vous ne souhaitez pas utiliser le transtockage et assurez-vous également que vous ne comparerez pas accidentellement les instances de B à C, vous devez restructurer votre hiérarchie de classes de la manière proposée par Scott Meyers à l'article 33 de More Effective C++. En réalité, cet élément concerne l'opérateur d'assignation, ce qui n'a aucun sens s'il est utilisé pour des types non liés. En cas d'opération de comparaison, il est logique de renvoyer false lors de la comparaison d'une instance de B avec C. 

Vous trouverez ci-dessous un exemple de code qui utilise RTTI et ne divise pas la hiérarchie des classes en feuilles de type concreate et en base abstraite.

La bonne chose à propos de cet exemple de code est que vous n'obtiendrez pas std :: bad_cast lors de la comparaison d'instances non liées (comme B à C). Néanmoins, le compilateur vous permettra de le faire comme vous le souhaitez, vous pouvez implémenter de la même manière l’opérateur <et l’utiliser pour trier un vecteur de différentes instances A, B et C.

vivre

#include <iostream>
#include <string>
#include <typeinfo>
#include <vector>
#include <cassert>

class A {
    int val1;
public:
    A(int v) : val1(v) {}
protected:
    friend bool operator==(const A&, const A&);
    virtual bool isEqual(const A& obj) const { return obj.val1 == val1; }
};

bool operator==(const A& lhs, const A& rhs) {
    return typeid(lhs) == typeid(rhs) // Allow compare only instances of the same dynamic type
           && lhs.isEqual(rhs);       // If types are the same then do the comparision.
}

class B : public A {
    int val2;
public:
    B(int v) : A(v), val2(v) {}
    B(int v, int v2) : A(v2), val2(v) {}
protected:
    virtual bool isEqual(const A& obj) const override {
        auto v = dynamic_cast<const B&>(obj); // will never throw as isEqual is called only when
                                              // (typeid(lhs) == typeid(rhs)) is true.
        return A::isEqual(v) && v.val2 == val2;
    }
};

class C : public A {
    int val3;
public:
    C(int v) : A(v), val3(v) {}
protected:
    virtual bool isEqual(const A& obj) const override {
        auto v = dynamic_cast<const C&>(obj);
        return A::isEqual(v) && v.val3 == val3;
    }
};

int main()
{
    // Some examples for equality testing
    A* p1 = new B(10);
    A* p2 = new B(10);
    assert(*p1 == *p2);

    A* p3 = new B(10, 11);
    assert(!(*p1 == *p3));

    A* p4 = new B(11);
    assert(!(*p1 == *p4));

    A* p5 = new C(11);
    assert(!(*p4 == *p5));
}
4
marcinj
  1. Je pense que ça a l'air bizarre:

    void foo(const MyClass& lhs, const MyClass& rhs) {
      if (lhs == rhs) {
        MyClass tmp = rhs;
        // is tmp == rhs true?
      }
    }
    
  2. Si la mise en oeuvre de l'opérateur == semble être une question légitime, envisagez d'effacer le type (pensez quand même à effacer le type, c'est une belle technique). Voici Sean Parent qui le décrit. Ensuite, vous devez encore faire une distribution multiple. C'est un problème désagréable. Voici une discussion à ce sujet.

  3. Pensez à utiliser des variantes plutôt que la hiérarchie. Ils peuvent faire ce genre de choses facilement.

0