为类层次结构重载operator ==的正确方法是什么?

时间:2009-11-06 22:42:19

标签: c++ operator-overloading

假设我有以下类层次结构:

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

同样,我仍然需要施放(并且感觉不对)。有没有一种首选的方法呢?

更新

到目前为止只有两个答案,但看起来正确的方式类似于赋值运算符:

  • 使非叶类抽象
  • 非叶子类中的非虚拟保护
  • 叶子类中的公共非虚拟

任何用户尝试比较两个不同类型的对象都不会编译,因为基本函数受到保护,而叶子类可以利用父类的版本来比较那部分数据。

5 个答案:

答案 0 :(得分:14)

对于这种层次结构,我肯定会遵循Scott Meyer的Effective C ++建议,并避免使用任何具体的基类。无论如何,你似乎都是这样做的。

我会将operator==实现为自由函数,可能是朋友,仅用于具体的叶节点类类型。

如果基类必须有数据成员,那么我会在基类(isEqual中提供一个(可能受保护的)非虚拟助手函数,比如派生类'operator==可以使用。

E.g。

bool operator==(const B& lhs, const B& rhs)
{
    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;
}

答案 1 :(得分:12)

前几天我遇到了同样的问题,我提出了以下解决方案:

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 :(得分:9)

如果您合理地假设两个对象的类型必须相同才能使它们相等,那么就有办法减少每个派生类所需的样板量。这遵循Herb Sutter's recommendation来保护虚拟方法并隐藏在公共接口后面。 curiously recurring template pattern (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

上的演示

答案 3 :(得分:5)

如果您不想使用强制转换,并且确保不会将B的实例与C实例进行比较,那么您需要按照Scott Meyers在更有效的C ++第33项中建议的方式重构您的类层次结构。实际上这个项目涉及赋值运算符,如果用于非相关类型,它实际上没有意义。在比较操作的情况下,在将B的实例与C进行比较时返回false是有意义的。

下面是使用RTTI的示例代码,并没有将类层次结构划分为concreate leafs和abstract base。

这个示例代码的好处是,在比较非相关实例(比如B和C)时,不会得到std :: bad_cast。仍然,编译器将允许您执行可能需要的操作,您可以以相同的方式实现operator&lt;并用它来排序各种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));
}

答案 4 :(得分:0)

  1. 我认为这看起来很奇怪:

    void foo(const MyClass& lhs, const MyClass& rhs) {
      if (lhs == rhs) {
        MyClass tmp = rhs;
        // is tmp == rhs true?
      }
    }
    
  2. 如果实现operator ==看起来像是一个合法的问题,请考虑类型擦除(无论如何考虑类型擦除,它是一种可爱的技术)。 Here is Sean Parent describing it. 然后你仍然需要做一些多次调度。这是一个令人不快的问题。 Here is a talk about it.

  3. 考虑使用变体而不是层次结构。他们可以轻松地完成这类工作。