Implementing polymorphic operator==() in C++ idiomatic way

167 views Asked by At

What I think I need is like a pure virtual function, except the base class does have an implementation which derived classes mustn't default to (and this rule must propagate). The opposite of final?

I have a handful of types derived from a common base. The base is a useful type and some of the derived types are derived from other derived types. I'll be working with references to the base class only, but I need a virtual operator==() which sees through this and calls hand-rolled comparisons appropriate to each situation. A sort of two-dimensional vtable for operator==().

It's important that implementations don't propagate to derived classes, because letting this happen accidentally could result in a comparison between incompatible types falling through to a base class implementation where they are compatible types and this could produce a false positive.

I mean to let specific functionally-equivalent cases compare equal despite them being expressed in different classes. I expect the problem extends to other operations, though, and perhaps my use of operator==() isn't agreeable here.

I don't claim to know C++ -- I'm just a C hack trying to be idiomatic.

Here's what I've worked out so far:

class base;
class foo;
class bar;
class baz;

#define COMPARE public: \
  virtual bool equal(base const &p) const; \
  virtual bool equal(foo const &p) const; \
  virtual bool equal(bar const &p) const; \
  virtual bool equal(baz const &p) const; \
  virtual bool operator==(base const &p) const { return p.equal(*this); }

class base {
  int a_;

 public:
  base(int a) : a_(a) {}

  COMPARE
};

class foo : public base {
  int b_;

 public:
  foo(int a, int b) : base(a), b_(b) {}

  COMPARE
};

class bar : public base {
  int c_;

 public:
  bar(int a, int c) : base(a), c_(c) {}

  COMPARE
};

class baz : public bar {
  int d_;

 public:
  baz(int a, int c, int d) : bar(a, c), d_(d) {}

  COMPARE
};

Now, thanks to COMPARE, all T::equal() must be implemented, and none of them are allowed to fall back on an earlier implementation. Also, every class has its own operator==() which calls the appropriate equal() for its own type (rather than the base class type).

What I want is to enforce these rules in the way that COMPARE does now, but without needing to remember that every derived class has to reference the macro, and ideally (to be C++-idiomatic) without using a macro at all.

What's the proper C++ way to do this?

3

There are 3 answers

1
wally On

I'm also still learning, but what you describe sounds a lot like it might need a double dispatch and/or visitor pattern.

For double dispatch, something like:

class base;
class foo;
class bar;
class baz;


class base {
    int a_;
public:
    base(int a) : a_(a) {}
    virtual bool operator==(const base&) const =0;
    virtual bool operator==(const foo&) const =0;
    virtual bool operator==(const bar&) const =0;
    virtual bool operator==(const baz&) const =0;
};

class foo : public base {
    int b_;
public:
    foo(int a,int b) : base(a),b_(b) {}
    bool operator==(const base&) const override;
    bool operator==(const foo&) const override;
    bool operator==(const bar&) const override;
    bool operator==(const baz&) const override;
};

class bar : public base {
    int c_;
public:
    bar(int a,int c) : base(a),c_(c) {}
    bool operator==(const base&) const override;
    bool operator==(const foo&) const override;
    bool operator==(const bar&) const override;
    bool operator==(const baz&) const override;
};

class baz : public bar {
    int d_;
public:
    baz(int a,int c,int d) : bar(a,c),d_(d) {}
    bool operator==(const base&) const override;
    bool operator==(const foo&) const override;
    bool operator==(const bar&) const override;
    bool operator==(const baz&) const override;
};

This already looks a lot like the macro option presented above. :)

From TC++PL 4th edition, section 22.3.1 talking about double dispatch, there is mention of perhaps rather using a precomputed lookup table. Something like

bool equal(const base& b1,const base& b2)
    {
        auto i = index(type_id(b1),type_id(b2));
        return intersect_tbl[i](b1,b2);
    }
2
Karlis Olte On

except the base class does have an implementation which derived classes mustn't default to (and this rule must propagate)

If that is the only problem, then as Ben Voigt mentioned in his comment - it is not a problem, because pure virtual functions can be implemented.

Also, once a derived class overrides, I need the same rule in place, that the override isn't used by further derived classes (if they must then they can always call it explicitly in their own implementation)

If that is the case then "idiomatic C++ way" would probably be not to use inheritance to model "derived" - "further derived". Inheritance is typically used to model either substitutability or to model polymorphism, but it is neither here. In other words: model "derived" - "further derived" by composition.

1
sh1 On

So this appears to be one way of forcing derived classes to provide their own implementations of specific functions:

template<typename T> struct sweep : public T {
  template <class... Args> sweep(Args&&... args) : T(args...) { }
  virtual bool equal(base const &p) const = 0;
  virtual bool equal(foo const &p) const = 0;
  virtual bool equal(bar const &p) const = 0;
  virtual bool equal(baz const &p) const = 0;
  virtual bool operator==(base const &p) const = 0;
};

class base { ... };

class foo : public sweep<base> {
  int b_;

 public:
  foo(int a, int b) : sweep(a), b_(b) {}

  ...
};

It still requires that the derived class remember to do something specific to constrain itself -- to use the sweep template to derive from the base class -- but it is at least C++ rather than C.

It also has the advantage that the template could refresh the default implementations rather than make them pure virtual; like so:

template<typename T> struct sweep : public T {
  ...
  virtual bool equal(base const &p) const { return false; }
  ...
};

On the grounds that without further guidance every comparison should fail. This is actually closer to what I need -- but not what I asked for.