如何在 C++ 中重载多态 == 和 != 运算符

Posted

技术标签:

【中文标题】如何在 C++ 中重载多态 == 和 != 运算符【英文标题】:How to overload polymorphic == and != operator in c++ 【发布时间】:2021-10-07 22:27:35 【问题描述】:
class Media 
public:
    bool operator==(const Media& other) const 
    bool operator!=(const Media& other) const 
;

class Book : public Media 
public:
    bool operator==(const Book& other) const  // commenting out this line solves this issue.
    bool operator!=(const Book& other) const 
;

class Game : public Media 
public:
    bool operator==(const Game& other) const 
    bool operator!=(const Game& other) const 
;

int main() 
    Book book;
    Game game;

    bool res = book == game;  // doesn't compile.

我有这 3 个类,它们必须定义自己的 == 和 != 运算符。但是我还必须使用这些运算符比较两个兄弟姐妹。

我本可以在子类覆盖的基类中编写一个(纯)虚函数,例如virtual bool equals(const Media& other) const。然后在基类Media 中的== 和!= 操作符定义中调用该函数。但是,当我在 Book 类中添加另一个 bool operator==(const Book& other) const 时,该功能就消失了(Game 类也是如此)。

现在我想使用这些运算符比较兄弟姐妹之间的比较,并且在这 3 个类中仍然有所有 6 个定义。如何让它发挥作用?

【问题讨论】:

比较BookGame 时是否要使用Media::operator==(const Media&) 运算符? 您有 3*3 个组合(用于==),您的预期结果是什么? Double dispatch 如果以这种方式具有可比性,则应在 Media 类上进行比较。我的意思是,如果您将它们纯粹视为媒体,那么您应该能够比较它们,因此只需实现 Media == 和 != 运算符。 我认为最重要的问题是为什么你想像这样比较不同的类型?如果不同的媒体应该是对称平等的,那么应该可以仅根据基类对信息的了解进行比较——而不是孩子。如果你把自己设计到了一个只有这样出路的角落,那么这种设计存在问题的味道很浓。 【参考方案1】:

您在 cmets 中提到,这种比较形式是一种强加限制(在子类型的兄弟姐妹之间进行比较)。如果您需要通过继承以某种方式执行此操作的强加限制,那么一种选择是实现基本签名并使用dynamic_cast。请注意,这不是一种干净 方法,但如果这是某种形式的分配,它可能是该问题的预期解决方案。

dynamic_cast 使用运行时类型信息 (RTTI) 来确定基类的实例是否实际上是派生类的实例。当您将它与指针参数一起使用时,它会在失败时返回nullptr——这很容易测试:

auto p = dynamic_cast<const Book*>(&other);
if (p == nullptr)  // other is not a book
  return false;

// compare books

您可以将它与virtual 函数一起使用来满足层次结构。但是,为了避免c++20 生成的对称operator==/operator!= 函数可能产生歧义,通常最好通过一个命名 virtual 函数而不是operator== 本身来做到这一点为了防止歧义:

class Media 
public:
  virtual ~Media() = default;

  bool operator==(const Media& other) const  return do_equals(other); 

private:
  virtual bool do_equals(const Media& other) const = 0;
;

class Book : public Media 
  ...
private:
  bool do_equals(const Media& other) const override 
    auto* p = dynamic_cast<const Book*>(&other);
    if (p == nullptr)  return false; 

    return (... some comparison logic ...);
  
  ...
;

... Same with Game ...

由于我们从未定义operator==(const Book&amp;)operator==(const Game&amp;),因此我们不会看到基类'operator== 的影子;相反,它总是通过 base 的 operator==(const Media&amp;) 进行调度——这不是virtual 并防止歧义。

这将使BookGame 具有可比性,但返回false——而两个Book 或两个Game 对象可以通过适当的逻辑进行比较。

Live Example


也就是说……

就软件架构而言,这种方法不是一个好的设计。它需要派生类来查询类型是什么——通常当你需要这样做时,这表明逻辑很时髦。当涉及到相等运算符时,它也会导致对称性的复杂化——不同的派生类可能会选择将事物与不同的类型进行奇怪的比较(想象一下Media 可能会将true 与其他不同的媒体进行比较;此时点,函数调用的顺序很重要)。

一般来说,更好的方法是在逻辑上需要相等比较的任何类型之间定义每个相应的相等运算符。如果你在 C++20 中,这很简单,对称等式生成;但是 pre-C++20 有点痛苦。

如果BookGame 相当,则定义operator==(const Game&amp;)operator==(const Book&amp;, const Game&amp;)。是的,这可能意味着您要为每个人定义大量的operator==s;但它更加连贯,并且可以获得更好的对称性(尤其是 C++20 的对称相等性):

bool operator==(const Game&, const Book&);
bool operator==(const Book&, const Game&); // Generated in C++20
bool operator==(const Game&, const Game&);
bool operator==(const Book&, const Book&);

在这样的组织中,Media 甚至可能不符合逻辑作为“基类”。考虑某种形式的静态多态性可能更合理,例如使用@Jarod42 的回答中提到的std::variant。这将允许类型被同构存储和比较,但不需要从基类型转换为派生类型:

// no inheritance:
class Book  ... ;
class Game  ... ;

struct EqualityVisitor 
  // Compare media of the same type
  template <typename T>
  bool operator()(const T& lhs, const T& rhs) const  return lhs == rhs; 

  // Don't compare different media
  template <typename T, typename U>
  bool operator()(const T&, const U&) const  return false; 
;

class Media

public:
  ...

  bool operator==(const Media& other) const 
    return std::visit(EqualityVisitor, m_media, other.m_media);
  
private:
  std::variant<Book, Game> m_media;
;

Live Example

这是我推荐的方法,前提是媒体的形式是固定的而不是扩展的。

【讨论】:

【参考方案2】:

感谢std::visit/std::variant (C++17),您可以进行双重调度:

class Media;
class Book;
class Game;

using MediaPtrVariant = std::variant<const Media*, const Book*, const Game*>;

class Media 
public:
    virtual ~Media () = default;
    virtual MediaPtrVariant asVariant() const  return this; 
;

class Book : public Media 
public:
    MediaPtrVariant asVariant() const override  return this; 
;

class Game : public Media 
public:
    MediaPtrVariant asVariant() const override  return this; 
;

struct EqualVisitor

    template <typename T>
    bool operator()(const T*, const T*) const  return true; 

    template <typename T, typename U>
    bool operator()(const T*, const U*) const  return false; 
;


bool operator ==(const Media& lhs, const Media& rhs)

    return std::visit(EqualVisitor(), lhs.AsVariant(), rhs.AsVariant());


bool operator !=(const Media& lhs, const Media& rhs)

    return !(lhs == rhs);


int main()

    Book book;
    Game game;

    bool res = book == game;

Demo

【讨论】:

以上是关于如何在 C++ 中重载多态 == 和 != 运算符的主要内容,如果未能解决你的问题,请参考以下文章

C++ 继承多态关系中的赋值运算符的重载=operator()

C++ 继承多态关系中的赋值运算符的重载=operator()

C++ 强制转换运算符重载和多态性

C++ 多态性 (polymorphism)

C++基础——C++面向对象之重载与多态基础总结(函数重载运算符重载多态的使用)

多态性——运算符重载