Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Class design complication (C++)

Tags:

c++

class

My classes are

  • Base
    • Derived_A
    • Derived_B
  • Parent
    • Child_One
    • Child_Two

Base has two signature functions:

virtual void foo( const Parent& ) = 0;
virtual void bar( const Base& ) = 0;

, which other parts of the program expect.

The problem is:

Derived_A treats Child_One and Child_Two the same. But Derived_B treats them differently.

How should I implement this?

One way is to find out what kind of object is passed to Derived_B.foo. This would be apparently "a design flaw". The other way I tried is to change the signature functions as:

class Base
{
  class Derived_A;
  class Derived_B;

//  virtual void bar( const Base& ) = 0;
  virtual void bar( const Derived_A& ) = 0;
  virtual void bar( const Derived_B& ) = 0;
}

class Derived_A: public virtual Base
{ 

  virtual void foo( const Parent& ) = 0;
}

class Derived_B: public virtual Base
{ 
  virtual void foo( const Child_A& ) = 0;
  virtual void foo( const Child_B& ) = 0;
}

But now the bar function cannot use Base.foo. So I have to write the bar function twice, although the code is exactly the same.

Are there any other ways to deal with the problem? which one do you suggest?

P.S. I couldn't think of a good title. Please feel free to modify it.

like image 783
Eliad Avatar asked Apr 16 '15 09:04

Eliad


3 Answers

The problem you are describing is called Double Dispatch. The link describes the problem and a few possible approaches to a solution (including polymorphic function signatures and the visitor pattern).

like image 83
utnapistim Avatar answered Oct 20 '22 20:10

utnapistim


Without details of what the two type hierarchies' relation is with each other and how they interact, it's impossible to say what approach is appropriate. I've composed an overview of the other answers and another viable alternative that can be extended to the visitor pattern which was mentioned in a comment.

Performing the polymorphic behaviour in the children implementing a virtual function in Parent as already suggested by Joey Andres is quite typical object oriented solution for this problem in general. Whether it's appropriate, depends on the responsibilities of the objects.

The type detection as suggested by Olayinka and already mentioned in your question certainly smells kludgy, but depending on details, can be the minimum of N evils. It can be implemented with member function returning an enum (I guess that's what Olayinka's answer tries to represent) or with a series of dynamic_casts as shown in one of the answers in the question you linked.

A trivial solution could be to overload foo in Base:

struct Base {
    virtual void foo(const Parent&) = 0;
    virtual void foo(const Child_Two&) = 0;
};
struct Derived_A: Base { 
    void foo(const Parent& p) {
        // treat same
    }
    void foo(const Child_Two& p) {
        foo(static_cast<Parent&>(p));
    }
};
struct Derived_A: Base { 
    void foo(const Parent& p) {
        // treat Child_One (and other)
    }
    void foo(const Child_Two& p) {
        // treat Child_Two
    }
};

If there are other subtypes of Base that treat Child_One and Child_Two the same, then the implementation of foo(const Child_Two&) may be put in Base to avoid duplication.

The catch of this approach is that foo must be called with a reference of proper static type. The call will not resolve based on the dynamic type. That may be better or worse for your design. If you need polymorphic behaviour, you can use the visitor pattern which essentially adds virtual dispatch on top of the solution above:

struct Base {
    foo(Parent& p) {
        p.accept(*this);
    }
    virtual void visit(Child_A&) = 0;
    virtual void visit(Child_B&) = 0;
};

struct Parent {
    virtual void accept(Base&) = 0;
};

struct Child_A: Parent {
    void accept(Base& v) {
        v.visit(*this);
    }
};
// Child_B similarly

struct Derived_A: Base { 
    void treat_same(Parent&) {
        // ...
    }
    void visit(Child_A& a) {
        treat_same(a);
    }
    void visit(Child_B& b) {
        treat_same(b);
    }
};
struct Derived_B: Base { 
    void visit(Child_A&) {
        // ...
    }
    void visit(Child_B&) {
        // ...
    }
};

There's a bit more boilerplate, but since you seem very averse to implementing the behaviour in the children, this may be good approach for you.

like image 43
eerorika Avatar answered Oct 20 '22 21:10

eerorika


You could've easily made a virtual foo method in Parent. Since you want Derive_A to treat all Parent's subclasses the same, why not implement a class that does just that in Parent. That is the most logical thing, since chances are, if you want to do the same to both of them, then both of them must have similar data, which is exist in Parent.

class Parent{
   virtual void treatSame(){
       // Some operations that treat both Child_A, and Child_B
       // the same thing to both Child_A and Child_B.
   }
   virtual void foo() = 0;
}

Since you want Derived_B to do different operations in both Child_A and Child_B, take advantage of polymorphism. Consider the rest of the classes below:

class Child_A : public Parent{
    virtual void foo(){
        // Foo that is designed for special Child_A.
    }
}

class Child_B : public Parent{
    virtual void foo(){
        // Foo that is designed for special Child_B.
    }
}


class Base{
     virtual void foo(Parent) = 0;
     virtual void bar(Base) = 0;
}

class Derived_A: public Base
{ 
  virtual void foo( Parent& p){
     p.treatSame();
  }
}

class Derived_B: public Base
{ 
  virtual void foo( Parent& p){
      p.foo();  // Calls appropriate function, thanks to polymorphism.
  }
}

A possible usage is the following:

int main(){
    Child_A a;
    Child_B b;

    Derived_A da;
    da.foo(a);  // Calls a.treatSame();
    da.foo(b);  // Calls a.treatSame();

    Derived_B db;
    db.foo(a);  // Calls a.foo();
    db.foo(b);  // Calls b.foo();
}

Note that this will only work when the parameters are pointer or reference (I prefer to deal with reference when possible). Virtual dispatch (selecting appropriate function) won't work otherwise.

like image 38
Joey Andres Avatar answered Oct 20 '22 22:10

Joey Andres