Wat is de juiste manier om operator== te overbelasten voor een klassenhiërarchie?

Stel dat ik de volgende klassenhiërarchie heb:

class A
{
    int foo;
    virtual ~A() = 0;
};
A::~A() {}
class B : public A
{
    int bar;
};
class C : public A
{
    int baz;
};

Wat is de juiste manier om operator==te overbelasten voor deze klassen? Als ik ze allemaal gratis functies maak, kunnen B en C de versie van A niet gebruiken zonder te casten. Het zou ook voorkomen dat iemand een diepgaande vergelijking maakt met alleen verwijzingen naar A. Als ik ze virtuele lidfuncties maak, kan een afgeleide versie er als volgt uitzien:

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;
    }
}

Nogmaals, ik moet nog steeds casten (en dat voelt verkeerd). Is er een voorkeursmanier om dit te doen?

Bijwerken:

Er zijn tot nu toe slechts twee antwoorden, maar het lijkt erop dat de juiste manier analoog is aan de toewijzingsoperator:

  • Maak niet-bladklassen abstract
  • Niet-virtueel beschermd in de niet-bladklassen
  • Openbaar niet-virtueel in de bladklassen

Een poging van een gebruiker om twee objecten van verschillende typen te vergelijken, wordt niet gecompileerd omdat de basisfunctie is beveiligd en de leaf-klassen de bovenliggende versie kunnen gebruiken om dat deel van de gegevens te vergelijken.


Antwoord 1, autoriteit 100%

Voor dit soort hiërarchie zou ik zeker het effectieve C++-advies van Scott Meyer volgen en geen concrete basisklassen gebruiken. Je lijkt dit in ieder geval te doen.

Ik zou operator==implementeren als een gratis functie, waarschijnlijk vrienden, alleen voor de concrete klassentypes met bladknooppunten.

Als de basisklasse dataleden moet hebben, dan zou ik een (waarschijnlijk beschermde) niet-virtuele helperfunctie in de basisklasse (isEqualbijvoorbeeld) geven die de afgeleide klassen’ operator==zou kunnen gebruiken.

Bijvoorbeeld

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

Door het vermijden van een operator==die werkt op abstracte basisklassen en door vergelijkingsfuncties beschermd te houden, krijgt u nooit per ongeluk fallbacks in clientcode waarbij alleen het basisgedeelte van twee verschillend getypeerde objecten worden vergeleken.

Ik weet niet zeker of ik een virtuele vergelijkingsfunctie zou implementeren met een dynamic_cast, ik zou dit niet doen, maar als er een bewezen noodzaak voor zou zijn, zou ik waarschijnlijk voor een pure virtuele functie in de basisklasse (nietoperator==) die vervolgens werd overschreven in de concrete afgeleide klassen als iets als dit, met behulp van de operator==voor de afgeleide klasse.

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

Antwoord 2, autoriteit 50%

Ik had laatst hetzelfde probleem en ik kwam met de volgende oplossing:

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;
    }
};

Het ding dat ik niet leuk vind aan dit is de typide cheque. Wat denk je erover?


Antwoord 3, Autoriteit 43%

Als u het gieten niet wilt gebruiken en ook zorgt ervoor dat u niet per ongeluk een exemplaar van B vergelijken met C met CHIENT VAN C, moet u uw klassenhiërarchie op een bepaalde manier herstructureren als Scott Meyers suggereert in item 33 van effectiever C++. Eigenlijk behandelt dit artikel opdrachtoperator, die echt geen zin heeft als gebruikt voor niet-verwante typen. In geval van vergelijking van de operatie is het logisch om false te retourneren bij het vergelijken van ex-instantie van B met C.

Hieronder is een voorbeeldcode die RTTI gebruikt en de klassenhiërarchie niet verdelen om doorbladert en abstracte basis te verbouwen.

Het goede ding over deze voorbeeldcode is dat u niet std :: bad_cast krijgt bij het vergelijken van niet-gerelateerde instanties (zoals B met C). Toch zal de compiler u toestaan ​​om het te doen, wat gewenst is, u kunt implementeren op dezelfde manier Operator & LT; en gebruik het voor het sorteren van een vector van verschillende A, B- en C-instanties.

wonen

#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));
}

Antwoord 4, autoriteit 32%

Als je er redelijkerwijs van uitgaat dat de typen van beide objecten identiek moeten zijn om gelijk te zijn, is er een manier om de hoeveelheid boilerplate die nodig is in elke afgeleide klasse te verminderen. Dit volgt op Herb Sutter’s aanbevelingom virtuele methoden te beschermen en te verbergen achter een openbare interface. Het curiously recurring template pattern (CRTP)wordt gebruikt om de boilerplate-code te implementeren in de equalsmethode zodat de afgeleide klassen dat niet hoeven te doen.

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;
};

Bekijk een demo op http://ideone.com/SymduV


Antwoord 5

  1. Ik vind dit er raar uitzien:

    void foo(const MyClass& lhs, const MyClass& rhs) {
      if (lhs == rhs) {
        MyClass tmp = rhs;
        // is tmp == rhs true?
      }
    }
    
  2. Als het implementeren van operator== een legitieme vraag lijkt, overweeg dan het wissen van typen (overweeg in ieder geval type wissen, het is een mooie techniek). Hier is Sean Parent die het beschrijft.
    Dan moet je nog wat meervoudige verzending doen. Het is een onaangenaam probleem. Hier is een gesprek over.

  3. Overweeg om varianten te gebruiken in plaats van hiërarchie. Ze kunnen dit soort dingen gemakkelijk doen.

Other episodes