Reading Assignment: All of Programming Chapter 18 Inheritance
C++, like many object-oriented languages, supports inheritance. Inheritance is the ability to declare a class, called the child class or subclass, derived class in such a way that it obtains all of the fields and methods of another class, called the parent class or superclass, base class. The child class can have more fields and methods of its own added to it, and can override the behavior of the method it inherited.
Inheritance is best used when two classes exhibit an is-a relationship. Contrast the is-a relationship between a derived class and a super class with a has-a relationship, a button has-a string for the text that appears on it, but we would not say that “A Button
is-a string”. Whenever two types exhibit as has-a relationship, inheritance is inappropriate.
Another Conceptual Example
Consider the BankAccount
class we have used in prior examples, we might have an InvestmentAccount
which has a list of stocks that are in the account in addition to the cash balance. In this case, inheritance is again appropriate-an InvestmentAccount
is-a BankAccount
. In our BankAccount/InvestmentAccount
class, we might have a special type of InvestmentAccount which allows people to “trade on margin”, called a MarginAccount
. In such a case, MarginAccount
could extend InvestmentAccount
. We can also have RetirementAccount
inherit from InvestmentAccount
.
Writing Classes with Inheritance
1 | class BankAccount{ |
In fact, if we do declare fields of the same name as those in the parent class, we end up with objects that have two different fields of the same name. They are distinct in that they have different fully qualified names. In this example, if we add a duplicated field called balance
, code in InvestmentAccount
would reference it by default, and would refer to the inherited field by BankAccount::balance
. However, creating multiple fields with the same name in this fashion is generally an indication of poor design or a lack of understanding of how inheritance works..
The public
access specifier in the inheritance declaration (: public BankAccount
specifies how the access of inherited members should be changed. Using public inheritance specifies that the access should be unchanged from that declared in the parent class: public members remain public, and private members remain private. One may use private inheritance by writing : private BankAccount
, in which all inherited members become private in the child class. Using protected inheritance makes public members of the parent class into protected members of the child class. Members of a class may be declared protect
, which mean that they may be accessed by members of the class, or by members of any of its child classes.
For example:
1 | class A{ |
The reference to x inside of B
is legal. The field x
is protected, so the child classes can access it. However, code which is outside of classes A
and B
would not be able to access x
directly. Of course, classes can also declare other classes as friend
s to grant them access to private/protected members.
Construction/Destruction
When objects that use inheritance are constructed or destroyed, the constructors and destructors for their parent classes are run to initialize/cleanup the parent portion of the objects. It just like a stack, the first one to be created is the last one to be destructed, which is also the most ancestor.
To be more concrete, consider the inheritance hierarchy in which we have class A, class B which inherits from A, and class C which inherits from B.
During destruction, the process happens in reverse, however, it stops if any parent class’s destructor is trivial.
If the programmer does not explicitly specify a call to the parent class’s constructor, then the default constructor is implicitly used. If there is not default constructor, or if the default constructor is private, then the compiler will produce an error. If the programmer wishes to call some other constructor explicitly, she writes the call to the parent class’s constructor as the first element of the initializer list, by writing the parent class’s name, then parenthesis with the argument list. For example:
1 | class BankAccount{ |
Subtype Polymorphism
Polymorphism allows the same code to operate on multiple types. In addition to parametric polymorphism, there is another form of polymorphism, which is related to inheritance, is subtype polymorphism. Subtype polymorphism arises when one type is subtype of another type, meaning that an instance of subclass is substitutable for an instance of base class. By the nature of inheritance, we are guaranteed that anything which is legal to do the parent class is also legal to do to the child class.
Note that in C++, polymorphism is restricted by the access modifier used in inheriting the parent class. If public inheritance is used, then polymorphism may be freely used anywhere. If private or protected inheritance is used, then polymorphism is only permissible where a field with that access restriction could be used.
In C++, subtype polymorphism allows the programmer to treat an instance of a child class as if it were an instance of one of its parent classes. However, polymorphism is only applicable when used with pointers or references. Concretely, if class A is public parent class of class B, then the following code is legal:
1 | void f(A* a){ |
When we deal with pointers or references to objects in the presence of polymorphism, it is important to understand the difference between the static type and the dynamic type of the object that it points at. The static type is the type obtained by the type checking rules of the compiler, which only uses the declared types of variables. The dynamic type of the object is the type of object that is actually pointed at.
For example, consider if we wrote BankAccount * b = new InvestmentAccount();
. Here, the static type of *b
is BankAccount
-b
is declared as a pointer to a BankAccount, so no matter what type it actually points at, the static type of *b
is always a BankAccount
. However, the dynamic type of *b
is InvestmentAccount
. If we drew the execution of this code by hand, we would see that the arrow in b
‘s box points at an InvestmentAccount
object.
This distinction is important because the compiler only works with the static types. When the compiler type checks the program, it must ensure that method calls are legal based only on the static types. If we tried to call b->buyStock(someStock, amount)
in the above example-as far as the compiler is concerned, b
points at a BankAccount
, and BankAccount
objects do not have a buyStock
method. Even in cases where it is obvious to a person looking at the code that dynamic type will always be some more specific type, the compiler does not use this fact during type checking.
Method Overriding
A child class may override a method it inherits from its parent class, specifying a new behavior for that method, rather than using the one it inherits.
The static type was used to determine the method to call. The approach of having the static type determine which method to call is called static dispatch, and is the default behavior in C++.
However, static dispatch disagrees with what we typically would want in the way we would use inheritance and polymorphism. Returning to our earlier example with MarginAccount
‘s buyStock
method, we certainly want the method call to dispatch to the overridden method anytime the owner attempts to buy stock.
The behavior we desire in this case(and most case) is dynamic dispatch, in which the dynamic type of the object determines which method to invoke. If we want a method to be dynamically dispatched, we have to declare it as virtual. If we changed our simpler example to use dynamically dispatched methods by declaring them virtual:
1 | class A{ |
Now the call to sayHi
is dynamically dispatched. When the static and dynamic types are the same, this change does not make a difference in which method is called. However, when the static and dynamic types differ, the result change.
Note that the declaration of the method as virtual must appear in the parent class. The reason for this requirement is that when the compiler compiles the call to ptr->sayHi()
, it only know the static type of ptr
. In this case, the static type of the object that ptr
points to is an A, so the compiler looks in the definition of class A to see whether sayHi
should be statically or dynamically dispatched. The compiler then generates different code based on whether the function is not virtual(in which case, it generates a direct call to A’s sayHi
), or virtual(in which case, it generates code to dynamically dispatch the call, which is a bit more complex). Note that once a method is declared virtual, it remains virtual in all child classes, even if not explicitly declared so.
Classes which contain virtual methods are never POD types, as they contain extra information to allow dynamic dispatch.
In C++, whenever you may use a class may participate in polymorphism, its destructor should be declared virtual. Declaring destructors as virtual whenever you uses classes polymorphically is important to avoid issues with improperly destroying objects. When you delete an object, the destructor call is dispatched according to the same rules as method calls. If the destructor in the static type of the object being destroyed is not virtual, then the destructor call is statically dispatched. If the destructor in that class is virtual, then the destructor call is dynamically dispatched.
If you want to call the parent class’s version of a method, you can do so, by explicitly requesting it with the fully qualified name. For example:
1 | class MarginAccount: public InvestmentAccount{ |
An overridden method may have a more permissive access restriction. For example, if the parent declares the method as private, the child could declare its overridden version as public. However, it cannot become more restrictive(you can not override a public method with a private one), that is, you can not override a public method with a private one. Additionally, an overridden method may change the return type in a covariant fashion-meaning that the return type in the subclass is a subtype of the return type in the superclass. For example,
The return type of functions in class Animal
is Animal *
, while in class Cat
, the return type is Cat *
. This change of return type makes sense in this example, since the Cat’s father and mother will be Cats, not just any type of Animal. Making the return type more specific in this fashion may be useful in code which uses the Cat class in a non-polymorphic fashion, as the compiler will know that the return value is a Cat *
. Note that if the method returned Animal
and Cat
, then this overriding would be illegal, as polymorphism only works on pointers or references. Attempting to do so would result in error message such as:
1 | invalid covariant return type for 'virtual Cat Cat::getFather()' |
Abstract Methods and Classes
Making use of inheritance would confer several advantages to our software design. We can could make use of polymorphism, allowing us to track all of the shapes in our system as an array of Shape *s
. We could then make use of dynamic dispatch to have method invocations result in the correct code being executed based on the actual type of shape that was created.
We may want to count the number of contained points in some shape, like Circle
class, Square
class. If we make these classes inherited from Shape
class. We have no idea how to count. Instead, we would like to do is declare the containsPoint
method in the Shape
class in a way that we tell the compiler “there is no way I can define this method in this class, but any child class of mine must override this method with a real implementation. Such a method is called an abstract method or a pure virtual member function. We declare a virtual member function as abstract by placing=0;
at the end of its declaration:
1 | class Shape{ |
Note that abstract method must be virtual, as it only makes sense to use them with dynamic dispatch. The whole point is that we can have a Shape *
(or Shape &
) and call containsPoint
on it, without specifying how we would do containsPoint
on a generic Shape. Now, each of our subclasses of Shape(Circle, Rectangle, and Triangle) will override the containsPoint
method as appropriate to their respective types of shapes.
When a class has an abstract method in it, that class becomes an abstract class. There are a few special rules that go along with abstract classes.
The first is that an abstract class cannot be instantiated. That is, you cannot do new Shape
, nor can you declare a variable to have type Shape
. However, you can declare variable or parameter to have type Shape *
or Shape &
. A Shape *
or a Shape &
can be used to polymorphically reference an instance of a concrete subclass of Shape-one which has provided actual implementations for all of its abstract methods, such as Circle, Rectangle, or Triangle.
The second rule is that any subclass of an abstract class is also abstract unless it defines concrete implementations for all abstract methods in its parents. If our design called for it, we could make an abstract subclass of Shape which does not define containsPoint
, and then make concrete subclasses of that class. Of course, we could also make a subclass which did define containsPoint
, but declared new abstract methods of its own, and such a class would also be abstract.
Two rules above work together to make an important guarantee to the compiler, that is, any object you actually instantiate will have an implementation for all of the methods declared in it. This rule is crucial to the usefulness of abstract classes. It means that whenever we have a Shape *
, we can call containsPoint
on it.
As with any other class, abstract classes can have constructors, and the constructors for abstract parent classes are executed in the same way as the constructors for any other parent classes. If the code in the constructor of an abstract class is such that it calls an abstract method, there is a problem-the dynamic type of the object is the abstract class and no implementation is available.
???
Inheritance and templates
As we discussed earlier, most features we see in programming languages are composable-we can mix them together, and they work exactly the way we would expect. For example, function parameters and references exhibit this property. If we know how to declare a function parameter, and we know how to declare a reference, we can combine the two, and declare a function parameter which is a reference-and it works exactly the way we expected. Unlike most pairs of features, templates are not fully composable with inheritance, mostly with respect those that relate to virtual methods. While this delves a little more into odd corners of the language that we typically like to go, we mention it to help you avoid surprises and the frustration that goes along with them.
Aspects That Are Composable
We can have a templated class inherit from another class, to inherit from an instantiation of a templated class, or to mix the two, that is having a templated class inherit from an instantiation of another templated class. Often we want to have a templated class inherit from a templated parent class, we want to keep the generality of the parent class-we can achieve this behavior by instantiating the parent class with the template parameter of the child:
1 | template<typename t> |
Here we are still instantiating std::vector
, we just happen to be instantiating it with T
, which is the template parameter of MyFancyVector
. Whenever we instantiate MyFancyVector
, the resulting class will inherit from std::vector
instantiated with the same argument as MyFancyVector
(that is, MyFancyVector<int>
will inherit from std::vector<int>
.
We will note that you can even parameterize a class in terms of what its parent is:
1 | template<typename T> |
This design is called a mixin and we will discuss it in Chapter 29.
It is also perfectly fine for a templated class to have virtual methods:
1 | template<typename T> |
Aspects That Are Not Composable
Generally speaking, virtual methods and templates interact in complex ways. Here, we just give you some rules to be wary of:
A templated method cannot be virtual. You can not declare a templated method to be virtual(do not confuse this with a method inside of a templated class, which can be virtual as we discussed above):
1 | class MyClass{ |
Attempting to do so will result in error message. If you want to have a variety of virtual methods with similar functionality in the base class, you can instead make a protected(or private) non-virtual template, and have non-templated methods call it:
1 | class MyClass{ |
A templated function cannot override an inherited method. Suppose we have a parent class:
1 | class Parent{ |
Now, suppose we write a child class with a method by the same name (and parameter list-whether or not the parameter list is the same due to template specialization or not):
1 | class Child: public Parent{ |
This Child class does not actually override the method of the same name from the Parent class. Instead, we have a non-virtual template method in the child class and inherit the virtual method from the parent class. If we execute the following code:
1 | Parent *p = new Child(); |
Then it will print “Parent::something”, This rule is something of a corollary of the previous rule, as the method would have to be virtual to override a virtual method-however, the language designers decided to make this method legal as a non-virtual method which does not override the parent, rather than illegal under the previous rule.
Virtual methods are specialized when an instance is made. In Section 17.3.3 we discussed how a templated function, class, or method is only type checked when it is specialized. However, This rule is only applies to non-virtual methods. If a method is virtual, then the compiler must specialize it whenever it must create an instance of the class.
Planning Your Inheritance Hierarchy
Here is a good general high-level approach to planning your inheritance relationships:
- Determine what classes you need and what members they have.
- Look for similarities between classes.
- Look to see if there are anything with natural “is-a” relationships that are not related by inheritance.
- Repeat Step2 and 3 until you run out of opportunities for good uses of inheritance.
- Determine which classes should be abstract. These are the classes which you cannot actually have “just” that type of thing without being more specific.
There are a few other general guidelines to think about in designing your inheritance relationships:
- In general, you want a common member as far “up” the inheritance hierarchy as possible(meaning in a parent class rather than a child class). Doing so avoids duplicating code. Of course, you should only put the field or method in the parent class if the parent type actually has that field or method.
- Make plentiful use of dynamic dispatch.