Basics of inheritance

Note

The main topic on this round is graphical user interfaces. However, Qt provides a lot of predefined classes that are inherited from other classes. Therefore, before starting with Qt, we will consider the inheritance mechanism of C++ in general. On this course, we will study this concept in a very cursory way. You will use inheritance only in such situations where it is told to you what to inherit and from where to inherit. On further programming courses (Programming 3: Techniques and Software Design), you will learn how to design and implement inherited classes by yourself.

In programming languages, inheritance is a mechanism that you can use to form new classes (subclasses, inherited classes) from an existing one (base class). The subclasses have the same properties as the base class, with some additional ones. This is a very informal and inaccurate definition of inheritance, but it is a place to start.

Let us have a look at a simple example that has been implemented with two classes and the main program. Everything in this example is written into one file in order to avoid include lines making to code listing longer and to keep the essential content easy to search. Of course, using the things you learned on the round before this, you would be able to implement this much nicer with both classes having their own definition and implementation files, and the main program written in a file of its own.

#include <iostream>
#include <string>

using namespace std;

//--------------------------------------------------------

class Vehicle {
  public:
    Vehicle(double speed, string const& colour);
    double get_speed() const;
    void set_speed(double speed);
    string get_colour() const;
    void report_travel(double time) const;

  private:
    double speed_;
    string colour_;
};

Vehicle::Vehicle(double speed, string const& colour):
   speed_(speed), colour_(colour) {
}

double Vehicle::get_speed() const {
   return speed_;
}

void Vehicle::set_speed(double speed) {
   speed_ = speed;
}

string Vehicle::get_colour() const {
   return colour_;
}

void Vehicle::report_travel(double time) const {
   cout << get_colour() << " vehicle, the speed of which is "
        << get_speed() << ", travels "
        << get_speed() * time / 3600.0 * 1000.0 << " m in "
        << time << " seconds "
        << endl;
}

//--------------------------------------------------------

class Car: public Vehicle {
  public:
    Car(double speed, string const& colour, string const& reg_num);
    string get_registration_number() const;
    void report_characteristics() const;

  private:
    string registration_number_;
};

Car::Car(double speed, string const& colour, string const& reg_num):
   Vehicle(speed, colour), registration_number_(reg_num) {
}

string Car::get_registration_number() const {
   return registration_number_;
}

void Car::report_characteristics() const {
   cout << "Police is looking for a car: colour " << get_colour()
        << ", registration_number " << get_registration_number()
        << endl;
}

//--------------------------------------------------------

int main() {
   Vehicle veh(20.0, "red");  // 20 km/h

   veh.report_travel(1.0);    // in 1.0 seconds
   veh.set_speed(50.0);       // 50.0 km/h
   veh.report_travel(2.5);    // in 2.5 seconds

   Car subaru(75.0, "blue-grey", "ABC-123");
   subaru.report_travel(1.0);
   subaru.set_speed(120.0);
   subaru.report_travel(2.5);
   subaru.report_characteristics();
}

In the example, the class Car is inherited from the class Vehicle. Logically, this means that all cars are vehicles, but all vehicles are not cars. Therefore, cars have the same properties as vehicles, but in addition to that, they have some additional properties that separate them from vehicles.

Or, in other words, you can do to cars the same things you can do to vehicles, but in addition to them, you can do to cars some things that are typical only to cars. Often, inheritance is depicted with a diagram like below.

Inheritance depicted with a diagram

The so-called is-a relationship is formed in C++ by using the following syntax in class definition to inherit the subclass from the base class:

class SubClass : public BaseClass { ... };

When applying public inheritance, all methods in the public interface of the base class automatically become public methods of the subclass. In reality, this means that you can target all the methods defined in the public interface of the base class to the objects of the subclass.

However, you cannot directly handle the member variables in the private part of the base class in the methods of the subclass. Instead, you can operate on these variables only by calling base class public methods.

You should also remember the syntax below that you can use in the initializing list of the subclass’s constructor to initialize the member variables inherited from the base class:

Car::Car(double speed, string const& color, string const& reg_num):
    Vehicle(speed, color), registration_number_(reg_num) {
}

Above, we call the constructor of the base class in the initializing list of the subclass. That call must be the first thing in the initializing list.

Inheritance in its basic form, shown here, is usable if there is a class that is almost what you need but lacks some operations in its public interface.

For example, we can imagine that we want a version of the C++ class string that has the operation is_palindrome in its public interface:

#include <iostream>
#include <string>

using namespace std;

class MyString: public string {
  public:
    MyString();
    MyString(string const& init_value);
    bool is_palindrome() const;
  private:
    // No private attributes needed in this example
};

MyString::MyString(): string("") {
}

MyString::MyString(string const& init_value): string(init_value) {
}

bool MyString::is_palindrome() const {
    string::size_type left = 0;
    while ( left < length() / 2 ) {
        if ( at(left) != at(length() - left - 1) ) {
            return false;
        }
        ++left;
    }

    return true;
}

int main() {
    MyString s1;
    MyString s2( string("abcba") );
    MyString s3(s2);

    cout << s1.is_palindrome() << endl;
    cout << s2.is_palindrome() << endl;
    cout << s3.is_palindrome() << endl;

    s1.append("ab");
    s2.append("z");
    s3.append("z");

    cout << s1.is_palindrome() << endl;
    cout << s2.is_palindrome() << endl;
    cout << s3.is_palindrome() << endl;

}

Unfortunately, it is not always that easy to do inheritance from complicated base classes, but the idea is well represented here.

Because all the objects of the subclass are also members of the base class, you can basically use them in all the same places as you use the base class objects. In practice, C++ is not that flexible. However, you can use a subclass object as a parameter to a function if the type of its formal parameter is a reference or a pointer to a base class object.

void process_vehicle(Vehicle const& v) {
    v.report_travel(15.0);
}

int main() {
   Vehicle veh(20.0, "red");
   Car subaru(75.0, "blue-grey", "ABC-123");
   process_vehicle(veh);
   process_vehicle(subaru);
}

When you get down to it, this is also a mechanism for trying to achieve the benefits of dynamic typing in a language that uses static typing.

Notes on inheritance

Inheritance is an interesting mechanism, which means that you might want to use it just because it is new. This can easily lead to silly solutions. Use inheritance sparingly and only when you can say with reason that it is a good solution.

This is a good base rule:

Only inherit the subclass Y from the base class X if you can explain to yourself how each Y is also an X.

For example, it is reasonable to inherit a car from a vehicle because each car is also a vehicle. It is not, however, reasonable to inherit a car from an engine, because a car is not an engine. Here, we have a so-called has-a relationship: a car has an engine. Therefore, the way the implementation mechanism should go is that the private part of the class Car has a member variable of the type Engine.

The example above is almost laughably obvious. However, the fact is that an inexperienced class designer will spring this trap in any situation that is slightly more confusing, even if a closer look at the situation might reveal it to be exactly the same as an is-a vs. a has-a situation.

The examples on inheritance given in this section are only a superficial take on all the inheritance mechanisms offered by C++. Every one of you should now have a basic understanding of what inheritance means.