Какой правильный способ перегрузить operator == для иерархии классов?

Предположим, что у меня есть следующая иерархия классов:

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

A::~A() {}

class B : public A
{
    int bar;
};

class C : public A
{
    int baz;
};

Какой правильный способ перегрузить operator== для этих классов? Если я создам им все свободные функции, то B и C не могут использовать версию A без кастования. Это также помешает кому-то сделать глубокое сравнение, имеющее только ссылки на A. Если я создам им виртуальные функции-члены, то производная версия может выглядеть так:

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

Опять же, мне все равно приходится бросать (и это неправильно). Есть ли предпочтительный способ сделать это?

Update:

До сих пор есть только два ответа, но похоже, что правильный путь аналогичен оператору присваивания:

  • Сделать неклассические классы абстрактными
  • Защищенный не виртуальный в неклассических классах
  • Публичная не виртуальная в классах листа

Любая попытка пользователя сравнить два объекта разных типов не будет компилироваться, потому что базовая функция защищена, а листовые классы могут использовать родительскую версию для сравнения этой части данных.

Ответ 1

У меня была такая же проблема на днях, и я придумал следующее решение:

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

То, что мне не нравится в этом, - проверка типа. Что вы думаете об этом?

Ответ 2

Для такого рода иерархии я определенно буду следовать рекомендациям Скотта Мейера "Эффективный C++" и избегать каких-либо конкретных базовых классов. Вы, кажется, делаете это в любом случае.

Я бы реализовал operator== как свободные функции, возможно, друзья, только для конкретных типов типов leaf-node.

Если базовый класс должен иметь элементы данных, я бы предоставил (возможно, защищенную) не виртуальную вспомогательную функцию в базовом классе (isEqual, скажем), который могли бы использовать производные классы operator==.

например.

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

Избегая использования operator==, который работает с абстрактными базовыми классами и сохраняет защищенные функции сравнения, вы никогда не получаете случайных резервов в клиентском коде, где сравнивается только базовая часть двух разных типизированных объектов.

Я не уверен, буду ли я реализовывать функцию виртуального сравнения с dynamic_cast, я бы не хотел этого делать, но если бы была доказанная необходимость, я бы, вероятно, пошел с чистой виртуальной функцией в базовый класс (не operator==), который затем переопределялся в конкретных производных классах как нечто подобное, используя operator== для производного класса.

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

Ответ 3

Если вы сделаете разумное предположение о том, что типы обоих объектов должны быть одинаковыми для того, чтобы они были равны, существует способ уменьшить количество котельной пластины, требуемой в каждом производном классе. Это следует за рекомендацией Herb Sutter, чтобы поддерживать виртуальные методы защищенными и скрытыми за публичным интерфейсом. любопытно повторяющийся шаблон шаблона (CRTP) используется для реализации кода шаблона в методе equals, поэтому производным классам это не нужно.

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

См. демонстрацию в http://ideone.com/SymduV

Ответ 4

Если вы не хотите использовать кастинг, а также убедитесь, что вы случайно не сравните экземпляр B с экземпляром C, вам необходимо перестроить иерархию классов таким образом, как предлагает Скотт Майерс в пункте 33 "Более эффективный С++". На самом деле этот пункт посвящен оператору присваивания, который действительно не имеет смысла, если используется для не связанных типов. В случае операции сравнения имеет смысл возвращать значение false при сравнении экземпляра B с C.

Ниже приведен пример кода, который использует RTTI и не делит иерархию классов на конкретизированные листы и абстрактную базу.

Хорошая вещь в этом примере кода заключается в том, что вы не будете получать std:: bad_cast при сравнении не связанных экземпляров (например, B с C). Тем не менее, компилятор позволит вам сделать это, что может быть желательно, вы можете реализовать таким же образом оператор < и использовать его для сортировки вектора различных экземпляров A, B и C.

live

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

Ответ 5

  • Я думаю, что это выглядит странно:

    void foo(const MyClass& lhs, const MyClass& rhs) {
      if (lhs == rhs) {
        MyClass tmp = rhs;
        // is tmp == rhs true?
      }
    }
    
  • Если оператор-оператор ~ кажется законным, рассмотрите стирание типа (считайте стирание стилей в любом случае, это прекрасная техника). Здесь описывает Шон Родитель. Затем вам все равно придется выполнять несколько диспетчеризации. Это неприятная проблема. Вот об этом говорят.

  • Рассмотрите возможность использования вариантов вместо иерархии. Они могут легко делать такие вещи.