Using Inheritance And Virtual Functions by (13 July 2000) |
Return to The Archives |
Introduction
|
C++ is notorious for having misleading keywords which mean completely different
things in different contexts. This was done on purpose, to minimize reserved
keywords in the language. As a result we have a lot of keywords like virtual,
public, protected the meanings of which are poorly understood by many people.
To clear this up, we will develop a conceptual vocabulary of basic object
oriented design concepts and then take a look at how C++ expresses each of them
using its heavily context reliant syntax. Doing so gives you a valuable feel for
what things like inheritance, pure virtual functions and so on really mean to
you when you design a program. When you are done reading, you should be able to
spot those constructs as they appear in code and read their meanings like an
open book. Hopefully, it will help you clear out any present confusion in your
code, allowing you to work at the level of intent instead of guessing which
keyword to use. As you should know, the important tools for object oriented design in C++ are: Encapsulation is achieved by creating classes which have no public data members (variables) , and only expose some of their member functions (methods) to the outside world. This is standard practice in C++. Always put all the variables in the private section of a class declaration and the interface(methods) into the public section. You may also have some private methods, inaccessible to the outside world. The inheritance system in C++ has two basic parts, Class inheritance which describes what happens to classes, and Member or method inheritance which is for adjusting how your member functions behave under inheritance. |
Class Inheritance: Has-A, Is-A
|
Let us first look at what kind of inheritance you can have in C++. There are
only three basic types - the ever present and popular public inheritance, the obscure
protected inheritance, and unrelated private inheritance. (More about why it is
unrelated later) Let us examine public inheritance first. I assume you are familiar with the syntax for inheritance. Its a simple matter of declaring your class as " class MyClass : public ParentClass {...}; ". But let us focus instead on what this means. In beginner's C++ texts they tell you that public inheritance will borrow all the public and protected members of the base class, and make them public and protected members of your class. This is all very nice and dandy, but still does not give you any clue as to what it means for your program design. Public inheritance always means Is - A. This is a very, very important fact to remember. Read it a few more times. By Is-A we mean that a class has a special relation to another class, e.g.. an Apple Is-A Fruit, a Car Is-A Machine, etc. Public inheritance implies a very close relationship between the parent class and the subclass, it implies that whenever somebody uses ParentClass , they could just as well use SubClass. SubClass Is-A ParentClass. (But not vice versa, if Apple is a subclass of Fruit you cannot polymorphically substitute a Fruit where an Apple is expected. This is somewhat counterintuitive at first, think it over a few times and it will make sense) Protected Inheritance Protected inheritance is not really used much in everyday life. It serves its special purpose - just takes public and protected members of the base class and makes them protected members of the subclass. It has no well defined meaning for single inheritance (there are uses for it in Multiple inheritance more about this in the next article, hopefully). Since it has no conceptual meaning, its best to avoid it especially in high level design decisions. Private Inheritance And finally Private inheritance. I called it "unrelated" earlier and here is why : by making public and protected members of the parent class private members our subclass, this inheritance technique destroys all conceptual ties between the subclass and parent class. The subclass is just implemented in terms of the parent, nothing more. Private inheritance is in fact just an implementation technique and implies no relationship between the classes involved. We arrive at a surprising conclusion : the public, protected and private inheritance tools in C++ have completely different, unrelated meanings. It almost makes no sense to study them taken together, yet this is the way most standard textbooks approach the subject. The champion is undeniably public inheritance, expressing the Is-A concept. You may be wondering, what's is so important about being able to substitute one object type for another. Well, this useless looking technique is the fundamental building block behind the whole science of Design Patterns, which facilitates creating reusable, robust object oriented designs. See references [2] and [3] at the bottom of this article for a more in-depth look at those subjects. Using Public Inheritance you can extend existing classes to do what you want them to in addition to, or instead of what they did before. Again, since the parent class only exposes an interface (methods) and no variables, you can modify some of the methods to do different things in your subclass, and leave the others to do their "default" thing, specified by the parent class. As you can see there's already a few different ways to make use of inheritance. How does C++ know which one you want? Well, if things were simpler, there would be keywords for each way, but this is of course impractical. C++ uses context based syntax that may seem complicated at first, but actually is very simple and allows you to express all of the concepts mentioned above and then some. |
Member Functions And Inheritance
|
There are three distinct ways to declare inherited
member functions in C++. Lets have a look at a bit of code here:
Each member function in this class shows a different way of passing on member function behavior to your subclasses (class Fish is obviously designed to be inherited from). The first member, fishColor() is whats called a "pure virtual" function. You have probably come across these before (if not, every decent c++ book has an explanation of the syntax). Let us instead focus on the actual meaning of making a member pure virtual. First of all, having one of those in your class declaration makes that class an Abstract class (you can't instantiate it). Second, and more importantly it specifies that all subclasses of Fish must implement their own versions of fishColor(). The syntax makes it mandatory. Class Fish does not give any implementation for fishColor() , and any class inheriting from it is obliged to declare the function and define code for its own version of fishColor(), or else your compiler will complain. Pure virtual functions correspond to inheritance of the interface only. Class Fish in effect says " Each of my subclasses has this function, and they will give their own implementations". The next function, wiggleFrequency() is a Simple Virtual function. Class Fish must define an implementation for wiggleFrequency(). Suppose the wiggle frequency of a fish in your application depends on some complicated skeletal weighting system, specific for each kind of fish. All subclasses of Fish inherit both the interface AND a default implementation for this function. They can still override this default behavior by simply defining their own version of this function, however if they do not, the default implementation from Class Fish will be used. (I just skipped this implementation code for Class Fish to save space) The last function is "isWaterCreature()". This is just your generic member function we have all grown to love. Class Fish provides an implementation for this function somewhere, and this implementation will be inherited by all subclasses of fish. Whats more, the subclasses are NOT allowed to change the implementation of this function. Subclasses are in fact forced to inherit both the interface and a mandatory implementation for this function from Fish. This table summarizes the things we just discussed:
One other possible relationship between classes is known as Has-A or containment. It means that one class contains objects of another class. For example, a CCircus Has-A CTigerTamer. In C++ Has-A relationships have nothing to do with inheritance. You can model Has-A by simply putting an instance object of type CTigerTamer within CCircus's declaration and you are done. Also, Has-A does not imply any kind of dynamic substitution / polymorphic properties. In a few rare situations you must use private inheritance to implement the Has-A relationship discussed earlier. In general, be careful when using this inheritance technique and take a moment to think it through. Could you just use Has-A containment to do the same thing? If the answer is yes, avoid private inheritance and go for the simpler containment solution. A common technique is utilizing Abstract Classes. They are declared like this :
In other words, all member functions of such classes are pure virtual. You can't instantiate this class, and its usefulness comes from building inheritance hierarchies based on its interface. With polymorphism and dynamic binding you can also achieve great generality and robustness in your code - these issues are explored further in the design patterns references given at the end. The basic idea is to publicly inherit a bunch of classes from SomeClass, and then make sure that the code using instances of these new classes does not in fact know about anything but SomeClass. Just for a taste of what can be done, imagine you have some sort of a list, which is declared to hold Base3DActor types. Suppose Base3DActor is an abstract class you built for your app, and you derive all your 3d entity types from this class like, Explosion : public Base3DActor , Tank : public Base3DActor .. etc. Now, if you apply the ideas shown in this article you can simply write a loop that goes through your Base3DActor list and calls some function Render() on each. Thanks to polymorphism each of the objects like explosions and tanks should render themselves properly. Even in this simple example you can begin to see the power and generality of the principle. |
Common Mistakes To Avoid
|
Lets just take a little detour and look at how you should NOT write code. A
common mistake people make is using either too much or too little "virtual" in
their code. Somebody might naively declare a class like this :
They then try to derive MySomeClass from it. This class is practically useless as far as inheriting from it goes, since its entire interface is enforced onto all descendants, with no customization allowed. But this is nothing compared to the kind of can of worms MySomeClass is. The programmer is obviously trying to add a specialized function func2() , which is supposed to do something other then the default one from SomeClass. Unfortunately, something unexpected will happen. The function that gets called depends solely on the type of pointer you are using to call it - no polymorphism. This is awful coding practice and should be avoided like hell, even though this code will compile, it will produce lots of nasty surprises and headaches.
Here the programmer declared every function to be a Simple Virtual function. Subclasses inherit the interface and a default implementation from SomeClass. There is nothing wrong with this code, but if this was a large class with dozens of member functions all of which are simple virtual, I would be suspicious. Its usually a sign of a misunderstanding. Overuse of virtual can lead to slower code and a larger memory footprint than necessary. Finally, as rule of thumb you should always make destructors in your base classes virtual if you use new/delete. This ensures that when somebody deallocates your base class object (like the Base3DObject example above), the base destructor will start a cascade of calls to the right subclasses, deallocating everything properly. Here are some references for further reading. [1] "Effective C++" - Scott Meyers [2] "Design Patterns" - Erich Gamma, Richard Helm, et al. [3] HUGI Magazine Issues 19, 18 - http://www.hugi.de/ |
|