Inheritance
Inheritance is defining a more specialized type based on an existing more general base type. The specialized type acquires the members of the base type and as a result can be substituted in place of the base type.
Inheritance is available for classes, not structs. The class that inherits another class is called the subclass, and the class that gets inherited is called the superclass, also called the base class.
There are two types of inheritance in D. We will cover implementation inheritance in this chapter and leave interface inheritance to a later chapter.
When defining a subclass, the superclass is specified after a colon character:
class SubClass : SuperClass { // ... }
To see an example of this, let's assume that there is already the following class that represents a clock:
class Clock { int hour; int minute; int second; void adjust(int hour, int minute, int second = 0) { this.hour = hour; this.minute = minute; this.second = second; } }
Apparently, the members of that class do not need special values during construction; so there is no constructor. Instead, the members are set by the adjust()
member function:
auto deskClock = new Clock; deskClock.adjust(20, 30); writefln( "%02s:%02s:%02s", deskClock.hour, deskClock.minute, deskClock.second);
Note: It would be more useful to produce the time string by a toString()
function. It will be added later when explaining the override
keyword below.
The output:
20:30:00
With only that much functionality, Clock
could be a struct as well, and depending on the needs of the program, that could be sufficient.
However, being a class makes it possible to inherit from Clock
.
To see an example of inheritance, let's consider an AlarmClock
that not only includes all of the functionality of Clock
, but also provides a way of setting the alarm. Let's first define this type without regard to Clock
. If we did that, we would have to include the same three members of Clock
and the same adjust()
function that adjusted them. AlarmClock
would also have other members for its additional functionality:
class AlarmClock { int hour; int minute; int second; int alarmHour; int alarmMinute; void adjust(int hour, int minute, int second = 0) { this.hour = hour; this.minute = minute; this.second = second; } void adjustAlarm(int hour, int minute) { alarmHour = hour; alarmMinute = minute; } }
The members that appear exactly in Clock
are highlighted. As can be seen, defining Clock
and AlarmClock
separately results in code duplication.
Inheritance is helpful in such cases. Inheriting AlarmClock
from Clock
simplifies the new class and reduces code duplication:
class AlarmClock : Clock { int alarmHour; int alarmMinute; void adjustAlarm(int hour, int minute) { alarmHour = hour; alarmMinute = minute; } }
The new definition of AlarmClock
is the equivalent of the previous one. The highlighted part of the new definition corresponds to the highlighted parts of the old definition.
Because AlarmClock
inherits the members of Clock
, it can be used just like a Clock
:
auto bedSideClock = new AlarmClock; bedSideClock.adjust(20, 30); bedSideClock.adjustAlarm(7, 0);
The members that are inherited from the superclass can be accessed as if they were the members of the subclass:
writefln("%02s:%02s:%02s ♫%02s:%02s", bedSideClock.hour, bedSideClock.minute, bedSideClock.second, bedSideClock.alarmHour, bedSideClock.alarmMinute);
The output:
20:30:00 ♫07:00
Note: An AlarmClock.toString
function would be more useful in this case. It will be defined later below.
The inheritance used in this example is implementation inheritance.
If we imagine the memory as a ribbon going from top to bottom, the placement of the members of AlarmClock
in memory can be pictured as in the following illustration:
│ . │ │ . │ the address of the object → ├─────────────┤ │(other data) │ │ hour │ │ minute │ │ second │ │ alarmHour │ │ alarmMinute │ ├─────────────┤ │ . │ │ . │
The illustration above is just to give an idea on how the members of the superclass and the subclass may be combined together. The actual layout of the members depends on the implementation details of the compiler in use. For example, the part that is marked as other data typically includes the pointer to the virtual function table (vtbl) of that particular class type. The details of the object layout are outside the scope of this book.
Warning: Inherit only if "is a"
We have seen that implementation inheritance is about acquiring members. Consider this kind of inheritance only if the subtype can be thought of being a kind of the supertype as in the phrase "alarm clock is a clock."
"Is a" is not the only relationship between types; a more common relationship is the "has a" relationship. For example, let's assume that we want to add the concept of a Battery
to the Clock
class. It would not be appropriate to add Battery
to Clock
by inheritance because the statement "clock is a battery" is not true:
class Clock : Battery { // ← WRONG DESIGN // ... }
A clock is not a battery; it has a battery. When there is such a relationship of containment, the type that is contained must be defined as a member of the type that contains it:
class Clock { Battery battery; // ← Correct design // ... }
Inheritance from at most one class
Classes can only inherit from a single base class (which itself can potentially inherit from another single class). In other words, multiple inheritance is not supported in D.
For example, assuming that there is also a SoundEmitter
class, and even though "alarm clock is a sound emitting object" is also true, it is not possible to inherit AlarmClock
both from Clock
and SoundEmitter
:
class SoundEmitter { // ... } class AlarmClock : Clock, SoundEmitter { // ← compilation ERROR // ... }
On the other hand, there is no limit to the number of interfaces that a class can inherit from. We will see the interface
keyword in a later chapter.
Additionally, there is no limit to how deep the inheritance hierarchy can go:
class MusicalInstrument { // ... } class StringInstrument : MusicalInstrument { // ... } class Violin : StringInstrument { // ... }
The inheritance hierarchy above defines a relationship from the more general to the more specific: musical instrument, string instrument, and violin.
Hierarchy charts
Types that are related by the "is a" relationship form a class hierarchy.
According to OOP conventions, class hierarchies are represented by superclasses being on the top and the subclasses being at the bottom. The inheritance relationships are indicated by arrows pointing from the subclasses to the superclasses.
For example, the following can be a hierarchy of musical instruments:
MusicalInstrument ↗ ↖ StringInstrument WindInstrument ↗ ↖ ↗ ↖ Violin Guitar Flute Recorder
Accessing superclass members
The super
keyword allows referring to members that are inherited from the superclass.
class AlarmClock : Clock { // ... void foo() { super.minute = 10; // The inherited 'minute' member minute = 10; // Same thing if there is no ambiguity } }
The super
keyword is not always necessary; minute
alone has the same meaning in the code above. The super
keyword is needed when both the superclass and the subclass have members under the same names. We will see this below when we will need to write super.reset()
and super.toString()
.
If multiple classes in an inheritance tree define a symbol with the same name, one can use the specific name of the class in the inheritance tree to disambiguate between the symbols:
class Device { string manufacturer; } class Clock : Device { string manufacturer; } class AlarmClock : Clock { // ... void foo() { Device.manufacturer = "Sunny Horology, Inc."; Clock.manufacturer = "Better Watches, Ltd."; } }
Constructing superclass members
The other use of the super
keyword is to call the constructor of the superclass. This is similar to calling the overloaded constructors of the current class: this
when calling constructors of the current class and super
when calling constructors of the superclass.
It is not required to call the superclass constructor explicitly. If the constructor of the subclass makes an explicit call to any overload of super
, then that constructor is executed by that call. Otherwise, and if the superclass has a default constructor, it is executed automatically before entering the body of the subclass.
We have not defined constructors for the Clock
and AlarmClock
classes yet. For that reason, the members of both of those classes are initialized by the .init
values of their respective types, which is 0 for int
.
Let's assume that Clock
has the following constructor:
class Clock { this(int hour, int minute, int second) { this.hour = hour; this.minute = minute; this.second = second; } // ... }
That constructor must be used when constructing Clock
objects:
auto clock = new Clock(17, 15, 0);
Naturally, the programmers who use the Clock
type directly would have to use that syntax. However, when constructing an AlarmClock
object, they cannot construct its Clock
part separately. Besides, the users of AlarmClock
need not even know that it inherits from Clock
.
A user of AlarmClock
should simply construct an AlarmClock
object and use it in the program without needing to pay attention to its Clock
heritage:
auto bedSideClock = new AlarmClock(/* ... */); // ... use as an AlarmClock ...
For that reason, constructing the superclass part is the responsibility of the subclass. The subclass calls the constructor of the superclass with the super()
syntax:
class AlarmClock : Clock { this(int hour, int minute, int second, // for Clock's members int alarmHour, int alarmMinute) { // for AlarmClock's members super(hour, minute, second); this.alarmHour = alarmHour; this.alarmMinute = alarmMinute; } // ... }
The constructor of AlarmClock
takes arguments for both its own members and the members of its superclass. It then uses part of those arguments to construct its superclass part.
Overriding the definitions of member functions
One of the benefits of inheritance is being able to redefine the member functions of the superclass in the subclass. This is called overriding: The existing definition of the member function of the superclass is overridden by the subclass with the override
keyword.
Overridable functions are called virtual functions. Virtual functions are implemented by the compiler through virtual function pointer tables (vtbl) and vtbl pointers. The details of this mechanism are outside the scope of this book. However, it must be known by every system programmer that virtual function calls are more expensive than regular function calls. Every non-private class
member function in D is virtual by default. For that reason, when a superclass function does not need to be overridden at all, it should be defined as final
so that it is not virtual. We will see the final
keyword later in the Interfaces chapter.
Let's assume that Clock
has a member function that is used for resetting its members all to zero:
class Clock { void reset() { hour = 0; minute = 0; second = 0; } // ... }
That function is inherited by AlarmClock
and can be called on an AlarmClock
object:
auto bedSideClock = new AlarmClock(20, 30, 0, 7, 0); // ... bedSideClock.reset();
However, necessarily ignorant of the members of AlarmClock
, Clock.reset
can only reset its own members. For that reason, to reset the members of the subclass as well, reset()
must be overridden:
class AlarmClock : Clock { override void reset() { super.reset(); alarmHour = 0; alarmMinute = 0; } // ... }
The subclass resets only its own members and dispatches the rest of the task to Clock
by the super.reset()
call. Note that writing just reset()
would not work as it would call the reset()
function of AlarmClock
itself. Calling reset()
from within itself would cause an infinite recursion.
The reason that I have delayed the definition of toString()
until this point is that it must be defined by the override
keyword for classes. As we will see in the next chapter, every class is automatically inherited from a superclass called Object
and Object
already defines a toString()
member function.
For that reason, the toString()
member function for classes must be defined by using the override
keyword:
import std.string; class Clock { override string toString() const { return format("%02s:%02s:%02s", hour, minute, second); } // ... } class AlarmClock : Clock { override string toString() const { return format("%s ♫%02s:%02s", super.toString(), alarmHour, alarmMinute); } // ... }
Note that AlarmClock
is again dispatching some of the task to Clock
by the super.toString()
call.
Those two overrides of toString()
allow converting AlarmClock
objects to strings:
void main() { auto deskClock = new AlarmClock(10, 15, 0, 6, 45); writeln(deskClock); }
The output:
10:15:00 ♫06:45
Using the subclass in place of the superclass
Since the superclass is more general and the subclass is more specialized, objects of a subclass can be used in places where an object of the superclass type is required. This is called polymorphism.
The concepts of general and specialized types can be seen in statements like "this type is of that type": "alarm clock is a clock", "student is a person", "cat is an animal", etc. Accordingly, an alarm clock can be used where a clock is needed, a student can be used where a person is needed, and a cat can be used where an animal is needed.
When a subclass object is being used as a superclass object, it does not lose its own specialized type. This is similar to the examples in real life: Using an alarm clock simply as a clock does not change the fact that it is an alarm clock; it would still behave like an alarm clock.
Let's assume that a function takes a Clock
object as parameter, which it resets at some point during its execution:
void use(Clock clock) { // ... clock.reset(); // ... }
Polymorphism makes it possible to send an AlarmClock
to such a function:
auto deskClock = new AlarmClock(10, 15, 0, 6, 45); writeln("Before: ", deskClock); use(deskClock); writeln("After : ", deskClock);
This is in accordance with the relationship "alarm clock is a clock." As a result, the members of the deskClock
object get reset:
Before: 10:15:00 ♫06:45
After : 00:00:00 ♫00:00
The important observation here is that not only the members of Clock
but also the members of AlarmClock
have been reset.
Although use()
calls reset()
on a Clock
object, since the actual object is an AlarmClock
, the function that gets called is AlarmClock.reset
. According to its definition above, AlarmClock.reset
resets the members of both Clock
and AlarmClock
.
In other words, although use()
uses the object as a Clock
, the actual object may be an inherited type that behaves in its own special way.
Let's add another class to the Clock
hierarchy. The reset()
function of this type sets its members to random values:
import std.random; class BrokenClock : Clock { this() { super(0, 0, 0); } override void reset() { hour = uniform(0, 24); minute = uniform(0, 60); second = uniform(0, 60); } }
When an object of BrokenClock
is sent to use()
, then the special reset()
function of BrokenClock
would be called. Again, although it is passed as a Clock
, the actual object is still a BrokenClock
:
auto shelfClock = new BrokenClock; use(shelfClock); writeln(shelfClock);
The output shows random time values as a result of resetting a BrokenClock
:
22:46:37
Inheritance is transitive
Polymorphism is not just limited to two classes. Subclasses of subclasses can also be used in place of any superclass in the hierarchy.
Let's consider the MusicalInstrument
hierarchy:
class MusicalInstrument { // ... } class StringInstrument : MusicalInstrument { // ... } class Violin : StringInstrument { // ... }
The inheritances above builds the following relationships: "string instrument is a musical instrument" and "violin is a string instrument." Therefore, it is also true that "violin is a musical instrument." Consequently, a Violin
object can be used in place of a MusicalInstrument
.
Assuming that all of the supporting code below has also been defined:
void playInTune(MusicalInstrument instrument, MusicalPiece piece) { instrument.tune(); instrument.play(piece); } // ... auto myViolin = new Violin; playInTune(myViolin, improvisation);
Although playInTune()
expects a MusicalInstrument
, it is being called with a Violin
due to the relationship "violin is a musical instrument."
Inheritance can be as deep as needed.
Abstract member functions and abstract classes
Sometimes there are member functions that are natural to appear in a class interface even though that class cannot provide its definition. When there is no concrete definition of a member function, that function is an abstract member function. A class that has at least one abstract member function is an abstract class.
For example, the ChessPiece
superclass in a hierarchy may have an isValid()
member function that determines whether a given move is valid for that chess piece. Since validity of a move depends on the actual type of the chess piece, the ChessPiece
general class cannot make this decision itself. The valid moves can only be known by the subclasses like Pawn
, King
, etc.
The abstract
keyword specifies that the inherited class must implement such a method itself:
class ChessPiece { abstract bool isValid(Square from, Square to); }
It is not possible to construct objects of abstract classes:
auto piece = new ChessPiece; // ← compilation ERROR
The subclass would have to override and implement all the abstract functions in order to make the class non-abstract and therefore constructible:
class Pawn : ChessPiece { override bool isValid(Square from, Square to) { // ... the implementation of isValid for pawn ... return decision; } }
It is now possible to construct objects of Pawn
:
auto piece = new Pawn; // compiles
Note that an abstract function may have an implementation of its own, but it would still require the subclass to provide its own implementation of such a function. For example, the ChessPiece
'es implementation may provide some useful checks of its own:
class ChessPiece { abstract bool isValid(Square from, Square to) { // We require the 'to' position to be different than // the 'from' position return from != to; } } class Pawn : ChessPiece { override bool isValid(Square from, Square to) { // First verify if it is a valid move for any ChessPiece if (!super.isValid(from, to)) { return false; } // ... then check if it is valid for the Pawn ... return decision; } }
The ChessPiece
class is still abstract even though isValid()
was already implemented, but the Pawn
class is non-abstract and can be instantiated.
Example
Let's consider a class hierarchy that represents railway vehicles:
RailwayVehicle / | \ Locomotive Train RailwayCar { load()?, unload()? } / \ PassengerCar FreightCar
The functions that RailwayCar
will declare as abstract
are indicated by question marks.
Since my goal is only to present a class hierarchy and point out some of its design decisions, I will not fully implement these classes. Instead of doing actual work, they will simply print messages.
The most general class of the hierarchy above is RailwayVehicle
. In this program, it will only know how to move itself:
class RailwayVehicle { void advance(size_t kilometers) { writefln("The vehicle is advancing %s kilometers", kilometers); } }
A class that inherits from RailwayVehicle
is Locomotive
, which does not have any special members yet:
class Locomotive : RailwayVehicle {
}
We will add a special makeSound()
member function to Locomotive
later during one of the exercises.
RailwayCar
is a RailwayVehicle
as well. However, if the hierarchy supports different types of railway cars, then certain behaviors like loading and unloading must be done according to their exact types. For that reason, RailwayCar
can only declare these two functions as abstract:
class RailwayCar : RailwayVehicle { abstract void load(); abstract void unload(); }
Loading and unloading a passenger car is as simple as opening the doors of the car, while loading and unloading a freight car may involve porters and winches. The following subclasses provide definitions for the abstract functions of RailwayCar
:
class PassengerCar : RailwayCar { override void load() { writeln("The passengers are getting on"); } override void unload() { writeln("The passengers are getting off"); } } class FreightCar : RailwayCar { override void load() { writeln("The crates are being loaded"); } override void unload() { writeln("The crates are being unloaded"); } }
Being an abstract class does not preclude the use of RailwayCar
in the program. Objects of RailwayCar
can not be constructed but RailwayCar
can be used as an interface. As the subclasses define the two relationships "passenger car is a railway car" and "freight car is a railway car", the objects of PassengerCar
and FreightCar
can be used in places of RailwayCar
. This will be seen in the Train
class below.
The class that represents a train can consist of a locomotive and an array of railwaycars:
class Train : RailwayVehicle { Locomotive locomotive; RailwayCar[] cars; // ... }
I would like to repeat an important point: Although both Locomotive
and RailwayCar
inherit from RailwayVehicle
, it would not be correct to inherit Train
from either of them. Inheritance models the "is a" relationship and a train is neither a locomotive nor a passenger car. A train consists of them.
If we require that every train must have a locomotive, the Train
constructor must ensure that it takes a valid Locomotive
object. Similarly, if the railway cars are optional, they can be added by a member function:
import std.exception; // ... class Train : RailwayVehicle { // ... this(Locomotive locomotive) { enforce(locomotive !is null, "Locomotive cannot be null"); this.locomotive = locomotive; } void addCar(RailwayCar[] cars...) { this.cars ~= cars; } // ... }
Note that addCar()
can validate the RailwayCar
objects as well. I am ignoring that validation here.
We can imagine that the departures and arrivals of trains should also be supported:
class Train : RailwayVehicle { // ... void departStation(string station) { foreach (car; cars) { car.load(); } writefln("Departing from %s station", station); } void arriveStation(string station) { writefln("Arriving at %s station", station); foreach (car; cars) { car.unload(); } } }
The following main()
is making use of the RailwayVehicle
hierarchy:
import std.stdio; // ... void main() { auto locomotive = new Locomotive; auto train = new Train(locomotive); train.addCar(new PassengerCar, new FreightCar); train.departStation("Ankara"); train.advance(500); train.arriveStation("Haydarpaşa"); }
The Train
class is being used by functions that are provided by two separate interfaces:
- When the
advance()
function is called, theTrain
object is being used as aRailwayVehicle
because that function is declared byRailwayVehicle
. - When the
departStation()
andarriveStation()
functions are called,train
is being used as aTrain
because those functions are declared byTrain
.
The arrows indicate that load()
and unload()
functions work according to the actual type of RailwayCar
:
The passengers are getting on ← The crates are being loaded ← Departing from Ankara station The vehicle is advancing 500 kilometers Arriving at Haydarpaşa station The passengers are getting off ← The crates are being unloaded ←
Summary
- Inheritance is used for the "is a" relationship.
- Every class can inherit from up to one
class
. super
has two uses: Calling the constructor of the superclass and accessing the members of the superclass.override
is for redefining member functions of the superclass specially for the subclass.abstract
requires that a member function must be overridden.
Exercises
- Let's modify
RailwayVehicle
. In addition to reporting the distance that it advances, let's have it also make sounds. To keep the output short, let's print the sounds per 100 kilometers:class RailwayVehicle { void advance(size_t kilometers) { writefln("The vehicle is advancing %s kilometers", kilometers); foreach (i; 0 .. kilometers / 100) { writefln(" %s", makeSound()); } } // ... }
However,
makeSound()
cannot be defined byRailwayVehicle
because vehicles may have different sounds:- "choo choo" for
Locomotive
- "clack clack" for
RailwayCar
Note: Leave
Train.makeSound
to the next exercise.Because it must be overridden,
makeSound()
must be declared asabstract
by the superclass:class RailwayVehicle { // ... abstract string makeSound(); }
Implement
makeSound()
for the subclasses and try the code with the followingmain()
:void main() { auto railwayCar1 = new PassengerCar; railwayCar1.advance(100); auto railwayCar2 = new FreightCar; railwayCar2.advance(200); auto locomotive = new Locomotive; locomotive.advance(300); }
Make the program produce the following output:
The vehicle is advancing 100 kilometers clack clack The vehicle is advancing 200 kilometers clack clack clack clack The vehicle is advancing 300 kilometers choo choo choo choo choo choo
Note that there is no requirement that the sounds of
PassengerCar
andFreightCar
be different. They can share the same implemention fromRailwayCar
. - "choo choo" for
- Think about how
makeSound()
can be implemented forTrain
. One idea is thatTrain.makeSound
may return astring
that consists of the sounds of the members ofTrain
.