Какой правильный способ перегрузить 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?
}
}
-
Если оператор-оператор ~ кажется законным, рассмотрите стирание типа (считайте стирание стилей в любом случае, это прекрасная техника). Здесь описывает Шон Родитель.
Затем вам все равно придется выполнять несколько диспетчеризации. Это неприятная проблема. Вот об этом говорят.
-
Рассмотрите возможность использования вариантов вместо иерархии. Они могут легко делать такие вещи.