Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoiding unrequired function declarations/definitions when using inheritance in C++

After a long time of C-style procedural coding, I am just beginning to 'get' OOP. So I suspect there may be standard way of dealing with the situation I am facing. I have an application with the class hierarchy shown below:

#include <iostream>
using namespace std;

class A {
public:
  virtual int intf() { return 0;} // Only needed by B
  virtual double df() {return 0.0;} // Only needed by C
};    
class B : public A {
  int intf() {return 2;}
  // B objects have no use for df()
};    
class C : public B {
  double df() {return 3.14;}
  // C objects have no use for intf()
};    
int main(){
  // Main needs to instantiate both B and C.
  B b;
  C c;
  A* pa2b = &b;
  A* pa2c = &c;

  cout << pa2b->intf() << endl;
  cout << pa2b->df() << endl;
  cout << pa2c->intf() << endl;
  cout << pa2c->df() << endl;

  return 0;
}

Now this program compiles and runs fine. However, I have question about its design. Class A is the common interface and does not need to be instantiated. Class B and C need to be. Regarding the functions: intf() is needed by B but not C, and df() is needed by C but not B. If I make intf() {df()} pure virtual in A, then there is no reasonable definition of df() {intf()} for B {C}.

Edit: B and C share some data members and also some member functions other than f(). I have not shown it my stripped down code.

Finally, as is standard, my application needs to access both B and C through a pointer to A. So my question is: Is there a way to 'clean up' this design so that unrequired/empty member function definitions (such as I have done in declaration/definition of A) can be eliminated? There is a clear "IS-A" relationship between the classes. So even though I share every newbie's thrill about inheritance, I dont feel I have stretched my design just so I could use inheritance.

Background in case it helps: I am implementing a regression suite. Class A implements functions and matrices common to every regression (such as dependent and independent variables). Class B is logistic regression with two classes ('0' and '1') and defines cost functions, and training algorithm for two-class logistic regression. Class C is multi-class logistic regression. It extends class B by training for multiple classes using the "one-vs-all" algorithm. So in a sense C is a binary logistic regression if you think of your class of interest as positive examples and all others as negative examples. Then you do it for every class to implement multi-class regression. The functions (intf and df) in question return the output. In case of logistic regression, the return value is a vector, while for multiclass regression, it is a matrix. And, as stated above, B and C dont have any use for each others' return functions. Except that I cant seem to be able to eliminate redundant definitions in A (the regression class).

Thanks for your help.

like image 260
RDK Avatar asked Feb 19 '23 16:02

RDK


2 Answers

Look at the Liskov Substitution Principle (http://en.wikipedia.org/wiki/Liskov_substitution_principle). It states that subclasses must fulfill the same contract as the superclass. In your example, neither subclass does this. The "Is-A" relationship is not enough to justify inheritance.

One option would be to use a single template method something like this:

template <typename T>
class A<T> {
    T getValue();
}

class B : A<int> {
    int getValue();
}

class C: A<double> {
    double getValue();
}

this would allow the contract to be fulfilled by both subclasses while allowing the return type of the method to vary based on the subclass definition.

If you want to learn more object oriented programming "best practices", google "Robert Martin SOLID"

like image 138
ryan0 Avatar answered Apr 20 '23 01:04

ryan0


You touched one of the most controversial point of OOP: the is-a == derivation pattern, resulting int the "god object" anti-pattern, since everything is ultimately a child-of-god, with god knowing every method of everyone and having an "answer" (read "default implementation") for everything.

"Is-a" is not enough to justify inheritance, where no replace-ability exist, but in real world no object is really fully replaceable with another, otherwise it will not be different.

You are in the "land of nowhere" where the substitution principle doesn't work well, but -at the same time- virtual functions look the best tool to implement dynamic dispatch.

The only thing you can do come to a compromise, and sacrifice one of the two.

As far the situation looks like, since B and C have nothing in common (no shared useful methods), simply don't let those method to originate from A. If you have something to "share" is probably a runtime mechanism to discover the type of B or C before entering B related specific code or C related specific code.

This is typically done with a common base having a runtime-type indicator to switch upon, or just a virtual function (typically the destructor) to let dynamic_cast to be able to work.

class A
{
public:
    virtual ~A() {}

    template<class T>
    T* is() { return dynamic_cast<T*>(this); }
};

class B: public A
{
public:
    int intf() { return 2; }
};

class C: public A
{
public:
    double df() { return 3.14; }
};

int main()
{
    using namespace std;

    B b;
    C c;

    A* ba = &b;
    A* ca = &c;

    B* pb = ba->is<B>();
    if(pb) cout << pb->intf() << endl;

    C* pc = ca->is<C>();
    if(pc) cout << pc->df() << endl;
}
like image 26
Emilio Garavaglia Avatar answered Apr 20 '23 01:04

Emilio Garavaglia