Как правильно перегрузить 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;
    }
}

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

Обновление:

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

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

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


person Michael Kristofik    schedule 06.11.2009    source источник
comment
Это классическая проблема двойной отправки. Либо ваша иерархия известна заранее, и в этом случае вы должны написать n * (n - 1) / 2 функций, либо это не так, и вы должны найти другой способ (например, вернуть хеш объекта и сравнить хеши).   -  person Alexandre C.    schedule 01.02.2013


Ответы (5)


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

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

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

E.g.

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

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

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

bool B::pubIsEqual( const A& rhs ) const
{
    const B* b = dynamic_cast< const B* >( &rhs );
    return b != NULL && *this == *b;
}
person CB Bailey    schedule 06.11.2009
comment
Вам обязательно нужен оператор == в абстрактном классе, чтобы предоставить полиморфизм. Я не думаю, что этот ответ хорош, потому что он не решает проблемы. - person fachexot; 02.07.2014
comment
В общем, я думаю, что базовый класс должен определять перегрузку operator == (внутренне или через класс друзей не имеет значения), которая проверяет равенство typeid и вызывает абстрактную виртуальную функцию равенства, которую будет определять производный класс. В этой функции производный класс может даже использовать static_cast, потому что typeid уже проверен на то же самое. Преимущество состоит в том, что пользователь, который обычно должен использовать только интерфейс, может использовать более простой == для сравнения двух объектов вместо того, чтобы вызывать пользовательскую функцию. - person Triskeldeian; 01.05.2017

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

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

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

person mtvec    schedule 11.01.2010
comment
Я думаю, вы получите дополнительную помощь, разместив это как отдельный вопрос. Кроме того, вы должны рассмотреть ответ Конрада Рудольфа и подумать, действительно ли вам нужно использовать operator== таким образом. - person Michael Kristofik; 11.01.2010
comment
Вопрос к сообщению Конрада Рудольфа: в чем разница между виртуальным методом равенства и виртуальным оператором ==? AFAIK, операторы - это просто обычные методы со специальными обозначениями. - person mtvec; 12.01.2010
comment
@ Работа: они есть. Но неявное ожидание состоит в том, что операторы не выполняют виртуальных операций, если я правильно помню, что Скотт Мейерс сказал в «Эффективном C ++». Честно говоря, я больше не уверен, и книги у меня сейчас нет под рукой. - person Konrad Rudolph; 12.01.2010

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

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

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

в прямом эфире

#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));
}
person marcinj    schedule 11.09.2016
comment
Вы должны использовать static_cast вместо dynamic_cast. Как вы уже проверили typeid, это безопасно и быстрее. - person galinette; 06.10.2016

Если вы сделаете разумное предположение, что типы обоих объектов должны быть идентичными, чтобы они были равными, есть способ уменьшить количество шаблонов, необходимых в каждом производном классе. Это следует за рекомендацией Херба Саттера по защите виртуальных методов и их скрытию за общедоступным интерфейсом. Любопытно повторяющийся шаблон шаблона (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

person Mark Ransom    schedule 27.09.2014
comment
Исходя из вашего предположения, я думаю, что было бы эффективнее и безопаснее проверять равенство typeid в операторе базового класса и использовать статическое приведение непосредственно в функции equals. Использование dynamic_cast означает, что если у T есть другой производный класс, назовите его X, чтобы можно было сравнить объект типа T и X через базовый класс и найти их равными, даже если только общая часть T фактически эквивалентна. Возможно, в некоторых случаях это именно то, что вам нужно, но в большинстве других это будет ошибкой. - person Triskeldeian; 01.05.2017
comment
@Triskeldeian вы хорошо замечаете, но на каком-то уровне вы ожидаете, что производные классы будут выполнять свои обещания. Я считаю, что техника, которую я показываю выше, больше касается реализации на уровне интерфейса. - person Mark Ransom; 01.05.2017
comment
Что действительно важно, ИМХО, так это то, что разработчик осознает риски и допущения по любому из методов. В идеале я полностью согласен с вами, но с вашей практической точки зрения, учитывая, что я работаю в основном с относительно неопытными программистами, этот выбор может быть более опасным, поскольку он может привести к очень тонкой ошибке, которую трудно обнаружить, которая закрадывается неожиданно. - person Triskeldeian; 02.05.2017

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

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

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

person Denis Yaroshevskiy    schedule 17.05.2016