System.Object, CObject, and the Seductive Lure of Deep Inheritance
If you’ve been programming in .NET for more than about 2 minutes you know that all .NET objects implicitly derive from System.Object.
But did you know that prior to .NET, in the world of native C++, the same convention existed (and still exists) in MFC? I mean the infamous CObject, the joy and bane of many an MFC programmer’s existence. Every class in the MFC library (with very few exceptions) ultimately derives from CObject, which provides four basic services:
- Run-Time Class Information (RTCI, not to be confused with RTTI)
Now, back in MFC’s heyday, developers were encouraged to derive their domain classes from CObject, so as to leverage the benefits of the CObject boilerplate. So let’s say you created a class Vehicle to use in your application. The idea was that, if you were building an MFC application, you’d go ahead and derive your Vehicle class from CObject, and be able to do things like serialization, if you wanted. For a while (a short while) this was even regarded as somewhat of a best practice, especially for developers who toed the Microsoft company line. The message was: build your application using the full power of the MFC library. The hidden subtext: abandon platform-independent solutions like the C++ standard library and do everything using MFC.
The problem, of course, was that CObject was a mess, and nobody really used it unless they had to. Now, I don’t want to hear any geeks telling me that no, CObject was good, it was genius, it was brilliant. CObject was, is, and always will be crap, no offense to the team responsible for writing it, most of whom were brilliant.
But CObject failed - miserably - in its role as a universal base class. Because the services it provided just weren’t that compelling:
- How often do you write a class that requires explicit serialization? And if you did, would you trust MFC’s serialization, or use something else?
- How often do you really need cooked-in CObject diagnostics, when it’s so easy to roll your own?
- How often do you write a class that requires the (dubious) powers of explicit run-time class information? (or RTCI, not to be confused with RTTI, run-time type information) If you’re writing a lot of C++ code that has to explicitly check the type of an object at run-time, odds are you need to think about refactoring your code.
For all these reasons and more, CObject never really caught on as a “universal base class for developer-created classes”. In practice, we worked with CObject because we were working with other classes, such as derived window or control classes, that themselves derived from CObject. To derive from virtually any MFC class is to ultimately derive from CObject, way up at the top of the inheritance hierarchy.
Now, around the time that MFC was gaining in popularity, developers were already realizing that deep inheritance hierarchies are evil. Herb Sutter explains in his excellent work, Exceptional C++:
Incidentally, programmers in the habit of making this mistake (using public inheritance for implementation) usually end up creating deep inheritance hierarchies. This greatly increases the maintenance burden by adding unnecessary complexity, forcing users to learn the interfaces of many classes even when all they want to do is use a specific derived class. It can also have an impact on memory use and program performance by adding unnecessary vtables and indirection to classes that do not really need them. If you find yourself frequently creating deep inheritance hierarchies, you should review your design style to see if you’ve picked up this bad habit. Deep hierarchies are rarely needed and almost never good. And if you don’t believe that but think that “OO just isn’t OO without lots of inheritance,” then a good counter-example to consider is the [C++] standard library itself.
The MFC library is a classic example of the problems associated with deep inheritance hierarchies. In order to use a derived class such as a CListView, you have to know how each layer of the inheritance hierachy works:
You end up with a sprawling in-memory layout of a particular object, with responsibilities divided (often unevenly) among the different layers of the hierachy. That sounds a little abstract, so let me tell you how it works in practice. In practice, you’re sitting there working with your CListView, trying to do something like cause it to refresh, or handling print preview, or some aspect of message routing, and you can’t remember where (at what level of the hierarchy) the particular service you’re looking for lives. So it’s another trip to MSDN, or fiddling around with Intellisense to figure out, aha, that particular feature lives in CWnd.
In other words, deep hierarchies are a big, confusing mess, and slapping a universal “I Am Object” base class on them doesn’t fix the problem.
So why, if that’s the case - if deep inheritance hiearchies are evil, and universal base classes along with them - why do I believe System.Object to be a brilliantly intuitive and effective universal base class? What did the .NET framework do right, that MFC got wrong?
For the answer to that, you’ll have to wait, albeit with less than bated breath, for Part Two.