重载类层次结构中的操作符 = = 的正确方法是什么?

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

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

再说一次,我仍然需要施法(感觉不对)。有没有更好的方法来做这件事?

更新:

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

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

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

134238 次浏览

对于这种层次结构,我肯定会遵循 Scott Meyer 的高效 C + + 建议,避免使用任何具体的基类。无论如何,你似乎正在做这件事。

我将 operator==作为一个免费函数来实现,可能是朋友,只针对具体的叶节点类类型。

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

例如。

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

有一天我遇到了同样的问题,我想出了以下的解决办法:

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

我不喜欢这种类型的支票,你觉得怎么样?

如果您合理地假设两个对象的类型必须相同才能相等,那么有一种方法可以减少每个派生类中所需的锅炉板的数量。这遵循 Herb Sutter 的推荐信,以保护虚拟方法并将其隐藏在公共接口之后。化学废物处理奇异递归模板模式(化学废物处置计划)用于在 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的演示

  1. 我觉得这看起来很奇怪:

    void foo(const MyClass& lhs, const MyClass& rhs) {
    if (lhs == rhs) {
    MyClass tmp = rhs;
    // is tmp == rhs true?
    }
    }
    
  2. If implementing operator== seems like a legit question, consider type erasure (consider type erasure anyways, it's a lovely technique). Here is Sean Parent describing it. Then you still have to do some multiple-dispatching. It's an unpleasant problem. Here is a talk about it.

  3. Consider using variants instead of hierarchy. They can do this type of things easyly.

如果你不想使用强制类型转换,并且确保你不会偶然地将 B 的实例和 C 的实例进行比较,那么你需要像 Scott Meyers 在《更有效的 C + + 》第33条中建议的那样重新构造你的类层次结构。实际上,此项处理赋值运算符,如果用于非相关类型,则这种运算符真的没有意义。在比较操作的情况下,当比较 B 和 C 的实例时,返回 false 是有意义的。

下面是使用 RTTI 的示例代码,它没有将类层次划分为聚合叶和抽象基。

这段示例代码的好处是,当比较非相关实例(如 B 和 C)时,不会得到 std: : bad _ cast。尽管如此,编译器仍然允许您按照需要进行操作,您可以以相同的方式实现操作符 < ,并使用它对各种 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));
}