Implementation inheritance in C++ is when you have a class that has both:
- non-abstract functions
- virtual functions
struct Vector3 { ... };
This is a value object, with no virtual functions, and can work fine as a container of plain data. (Maybe even use unique pointers, shared pointers, and such.)
class IMyInterface {
public:
virtual void MyFunction() = 0;
protected:
virtual ~MyFunction() {} // avoid compiler warnings
};
This is an interface class. (C++ doesn’t have “interfaces” per se, but these PABC Pure Abstract Base Classes will serve)
Those are both fine.
This is a bad API:
class MyThing {
public:
MyThing(lots of parameters);
~MyThing();
virtual void DoSomeThing();
virtual void DoSomeOtherThing();
};
Why is this bad?
Well, someone who overrides DoSomeThing doesn’t know whether they’re intended to call the superclass function or not. It’s also not part of the specification whether DoSomeOtherThing() will call DoSomeThing() or not.
There are more subtle design problems that end up happening as the inheritance chain gets deeper, and the pressure you feel towards creating a “god object” base class that has all the functionality comes from the problems of implementation inheritance.
A better alternative is to use the object simply as a collection of API functions (like a module) and use specific hooks/callbacks where necessary (this is delegation):
class MyThing {
public:
MyThing(lots of parameters);
~MyThing();
void DoSomeThing();
void DoSomeOtherThing();
void AddDelegate(ISomeThingHook *hook);
void RemoveDelegate(ISomeThingHook *hook);
};
class ISomeThingHook {
public:
// if returning false, abort the thing. state is the current number of geegaws.
virtual bool OnDoingSomeThing(int state) = 0;
};
This more cleanly separates the “implementation of my module on my data” of the base class, from “well defined variation points” in the hooks/delegates.
Still, this ends up with a fair bit of pointer chasing. If you can get away with POD structs and free functions, you’ll be better off, and your software will miss cache less often, too! And if you define your interfaces in terms of “large contiguous vectors of data” rather than “one thing at a time,” you end up parallelizing better, which works well both for SIMD, and for threading, and for GPU ports, depending on what specifically your code is doing.
There are other languages where these ways of designing are less cumbersome and more built-in, leading to less boilerplate. Dropping all subtlety, it approximately ends up as: “Haskell good; Java bad.”