Still not convinced?
20 Oct 2024
C++ has gone wildly out of control making inheritance a very difficult affair. With C++ Illuminati, either not mentioning The Rule of Three, or mentioning it but with almost no comment. AFAIK Scott Meyers did the right thing. He left. Oh no, not just because of this “C++ design flaw”.
23 Feb 2019
As I have claimed since ages ago, inheritance as a software composition method is evil.
There is no reason to use class inheritance.
And I am not the inventor of this advice. What more can we say? Ah yes, please read this little text first. Immediately. Then come back here.
What is Subclassing
That is “Code reuse”, aka Implementation Inheritance. And that is the bad one. Some people believe that the purpose of inheritance is code reuse. That is wrong. As stated plainly, “inheritance is not to be used for code reuse.”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using namespace std ; // class Parser { public: int version = 1 ; array parse ( string text, string delimiter ) { } } // class TextDoc : Parser { public: sting text_ ; void analyze () { parse(text_) ; } } |
TextDoc
is NOT a Parser
. Lazy contractor thinking: “But here is this Parser
class that was bought in from a third party and I am lazy and I “just inherit” it. So that I can analyze the text as clients do require.” But. If Parser
changes TextDoc
code must be recompiled too. But who cares? He is already on the next contract. Even worse; if Parser
version 2.0.0 has a new hidden bug nothing changes and nobody notices TextDoc
has a bug too inherited from a Parser. That is because TextDoc
and a Parser
are tightly coupled. Subclassing kind-of Inheritance is tight coupling.
Subtyping
This is a bit better. Think Interfaces. Use the inheritance of base types as interfaces for their implementation. Sadly C++ has no “interface” construct. It has an Abstract Base Class. But MSVC has the “interface” for you. Please use it if you can.
1 2 3 4 5 6 7 8 9 10 11 12 |
// MSVC // docs.microsoft.com/en-us/cpp/cpp/interface __interface ICount { int & next_increment( ); }; // sub-typing class MyCounter final : public ICount { public: int & next_increment () { static int val_ = 0 ; return val_++ ; } }; |
Inheritance in there has its purpose. It is used to implement the behaviour. Behaviour is expressed as an interface.
final is a C++ keyword since C++11 and its introduction is excellent news for people who wish to stop other people from abusing inheritance. So that (for example) a lazy contractor from use-case 1 cannot abuse our counter:
1 2 |
// modern C++ compiler error: MyCounter can not be inherited from // Does not compile --> class TextDoc : Parser, MyCounter { /* */ }; |
Usually mentioned with it, but not tightly coupled to inheritance is:
Polymorphism
A promise of substitutability: ie TextDoc
and BinaryDoc
are both Document
. Thus we can “call them documents”. We can generalize the code designs. But pay attention: this is just a promise. In reality very rarely one can find pure polymorphic behaviour in mature code, after years of code maintenance. That requires two things mostly missing from most projects started in a rush: discipline and vision. (a.k.a. “The Design”)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// MSVC keyword, otherwise C++ ABC __interface Document { bool open(string name) ; }; class TextDoc final : public Document { bool open(string name) { return true; } }; class BinaryDoc final : public Document { bool open(string name) { return true; } }; // open any kind of doc // *doc is polymorphic pointer to the root abstraction // compiler can and will discover what subtype is sent in here bool opendoc(Document * doc, string name) { return doc->open(name); } // int main() { std::unique_ptr<Document> text = new TextDoc{} ; auto result = opendoc(text.get(), "world oyster"); return 0; } |
Above we do not inherit for code reuse. Instead, we “sub-type”. We capture the relationship from reality into the design and we implement it and use it. The functionopendoc
does not care what kind of doc is opening. It opens any subtype of the type Document. Current and future ones. And that is important.
That is polymorphic behaviour. It allows us to use the behaviour of a base type (aka base class) while operating on derived types (aka derived classes).
Why exactly, do we declare and use IDocument
interface? Because that decouples us from the particular Document internals. And most importantly that allows for feasible design changes. For example, we could decide in the future to be able to incorporate and use online documents.
1 2 3 4 5 |
// change added some time after original // code has been delivered class OnlineDoc final : public Document { bool open(string name) { return true; } }; |
Now new clients can use the new type of doc
1 2 3 4 5 6 7 8 9 |
int main() { std::unique_ptr<Document> text = new OnlineDoc{} ; auto result = // opendoc requires no change whatsoever opendoc(text.get(), "https//online.example.com"); return 0; } |
The crucial bit is: that we do not require old users to recompile their legacy code using our library, just because we added yet another type of document.
Feasibility as a conclusion
Sometimes it is simply not possible to change anything in the code. That means, your management has understood there are very expensive changes, a.k.a rewrites. Not feasible.
In contrast to that, the sub-typing-based code/design above also has one more very important quality: Resilience to change. It is feasibly changeable. Changing that code is easily manageable. From both developers’ and clients’ and users’ points of view.
That put together delivers feasible development. Thank you and goodbye.
What, there is more? There is indeed more. If you are interested that is.
