10.2.2. Using Inheritance

Time: 00:07:21 | Download: Large, Large (CC), Small | Streaming (CC) | Slides (PDF)
Review

The following section builds on previously introduced concepts and terms. Please review the following as needed:

An inheritance relationship captures some aspects of a real-world problem. But, when we use inheritance in a computer program, it becomes genuinely beneficial. First, let's see what inheritance tells us when we model a real-world problem with object orientation.

A UML class diagram with Shape as the superclass, and Circle and Rectangle as subclasses. Shape has three private integer attributes: color, x, and y, and one public member function named set_color. Circle has a private integer variable named radius, and Rectangle has two private member variables named width and height.
An inheritance hierarchy. A Circle is-a Shape, and a Rectangle is-a Shape. Both subclasses inherit the features of the Shape superclass.

Two inheritance notations are in common use.

A frequently used example is a simple program to draw shapes on a computer screen. As illustrated in the adjacent figure, we could easily represent those shapes with an inheritance hierarchy or tree. When specifying classes that participate in an inheritance relationship, the best practice is to "hoist" all attributes and operations as high in the hierarchy as possible so that they apply to all of the subclasses. For example, both a Circle and a Rectangle have a rendering color and an x and y location on the screen. So, we hoist or lift those attributes to the Shape class. Now Shape, Circle, and Rectangle all have a color and an x, y location - without redefining those attributes in Circle, Rectangle, or any other Shape subclass. In this way, inheritance allows us to reuse code. This practice also means that if we ever need to correct or modify the code, there is only one copy, located in one class, that we must edit, rather than editing code in each subclass.

However, a Rectangle does not have a radius, and a Circle does not have a width or height (yes, we could describe a Circle with a circumscribing rectangle, but we're not going to do that). This observation means that it is not appropriate to hoist radius, width, or height to the Shape class as they do not apply to all Shape subclasses. Inheritance provides a clear statement of how two classes are similar while also clearly stating how those same classes are different. A Circle and a Rectangle are similar in that they both have a color and an x and y location; they are different in that a Circle has a radius while a Rectangle has a width and a height. Similarly, a Shape and a Circle are the same in that a Circle has everything that a Shape has, but a Circle is also distinct in that it has a radius that a Shape does not.

Even though we may not see it just yet, the distinction between which class "owns" some feature is the key to unlocking inheritance for us to use. However, before we turn that key, we must revisit some old concepts, acquire new terms, and clarify their meanings.

protected Visibility

Applied to object orientation, encapsulation denotes the practice and mechanisms of packaging data and the functions that operate on it in a way that hides or protects the data from the rest of the program - the only way to access the data is with the packaged functions. In this sense, encapsulation is a synonym for an object (if you have encapsulation, you have an object, and if you have an object, you have encapsulation). Encapsulation also separates the implementation of a function from the class's public interface. In C++, a public interface generally consists of the prototypes for all of the class's public functions (or, more accurately, the class's non-private features). Most C++ documentation includes this information. The documentation and the public interface focus on what a function does but not on how it does it. So, neither generally includes the function's implementation or body.

C++ controls the visibility or accessibility to the parts of a class with three keywords. The first two keywords, public and private, were introduced in the previous chapter. We delayed our coverage of the protected keyword until now because it only has meaning in the context of an inheritance relationship.

public
The feature (member data or member function) is visible and accessible throughout a program.
private
The feature is visible and accessible only from within an instance of the defining class (i.e., only in one of the class's member functions).
Although a subclass inherits all its superclasses' private members, it cannot "see" or access them directly. Instead, the subclass must use the public interface inherited from the superclass.
protected
Protected features are visible and accessible by the defining class and all its subclasses (as if the features were public). But protected features are completely invisible and inaccessible to classes that are not subclasses of the defining class (as if the features were private). UML class diagrams denote protected features with the "#" symbol.

Overloading vs. Overriding

Overloaded functions were introduced in chapter 6. Now, we are ready to consider overriding functions, requiring two classes related by inheritance.

A function override takes place when a function in a subclass has exactly the same signature (i.e., the same name, the same argument list, and the same return type) as a function in a superclass. In the case of the Shape and Circle example, we would say that the Circle draw function overrides the Shape draw function. The Circle class inherits everything that the Shape class has, including the draw function, but Circle adds its own draw function, replacing the Shape draw function. When a program calls the draw function through a Circle object, it calls the Circle function instead of the inherited function, but the Circle draw function can call the Shape draw function if needed.

The ability to overload or to override a function is strongly tied to the function's scope. For example, we don't define non-member functions in the scope of any class (although we may declare them in a namespace), so they may be overloaded but not overridden. Requiring overloaded functions to have at least one unique argument provides the compiler with enough information to match a function call with the correct function definition. But when we override functions, we deny the compiler this information because overridden functions must have the same arguments. We'll see a way of overcoming this problem in the polymorphism chapter. The adjacent figure illustrates the main differences between overloading and overriding functions.

Shape and Circle repeated but with different members illustrated. Circle has two overloaded functions named move, and both return void. One function has two integer arguments, and the other has only one. Shape and Circle each have a function named draw that returns void and has no arguments. The Circle draw function overrides the Shape draw function.
Overloaded vs. overridden functions. Functions are color-coded to the list at the left.

Chaining Overridden Function Calls

Understanding how to chain overridden function calls together is quite simple, but understanding why we want to do so is more difficult. It's easier to understand the purpose of chaining overridden functions if we recall three previous concepts. First, member functions typically use some or all of an object's data to solve some problem. Next, subclasses inherit all the superclasses' features (functions and variables). And finally, subclasses often have member variables that their superclasses do not.

Now, imagine that a superclass has a member function that solves a problem using one or more superclass member variables. Next, imagine that a subclass has an overridden function that solves the same kind of a problem but uses both subclass and superclass variables. Although the subclass inherits its superclass's member variables, it cannot access them if they are private. One possible way to solve this problem is to make the superclass's variables protected. But, the following example takes a different approach based on using the superclass's public interface by chaining overridden function calls.

A new UML class diagram showing SalariedEmployee as the superclass and SalesEmployee as the subclass. SalariedEmployee has a private member variable named salary and a public member function named calc_pay. SalesEmployee has two private member variables, total_sales and commission, and one public member function named calc_pay. The SalesEmployee class's calc_pay function overrides the SalaredEmployee class's calc_pay function.
SalariedEmployee
{
    private:
        double salary;
    public:
        double calc_pay()
        {
            return salary / 24;
        }
};

SalesEmployee : public SalariedEmployee
{
    private:
        double total_sales;
        double commission;	// percentage of sales
    private:
        double calc_pay()
        {
            return SalariedEmployee::calc_pay() + total_sales * commission;
        }
};
Chaining overridden function calls. A private member variable represents a salaried employee's annual salary. Common business practice is to pay salaried employees twice per month, which implies they are paid their salary/24 at each pay period. Another common practice is to pay sales personnel a commission or a percentage of their sales. In this example, a SalesEmployee is also a SalariedEmployee. So, paying a SalesEmployee both a salary and a commission is appropriate.

The highlighted expression illustrates how the SalesEmployee calc_pay function calls the SalariedEmployee calc_pay by chaining the function calls. The scope resolution operator specifies the call is to the SalariedEmployee function, avoiding an erroneous recursive call to the SalesEmployee function. The sales employee's commission is added to their salary to calculate their semi-monthly pay. The highlighted expression serves as an example of how to chain together all overridden functions.

Using Member Functions With Inheritance

Using member functions within an inheritance hierarchy is similar to using them within a single class. The Shape and Circle classes illustrate three "rules" for calling member functions defined in classes related by inheritance.

  1. When a subclass object calls an inherited function, a function not overridden by the subclass, it calls the superclass function. In this case, the call is indistinguishable from a call made to a subclass member function.
  2. When a subclass object calls an overridden function, it calls the subclass function unless the superclass function is specifically selected.
  3. An overriding function in a subclass may (and often does) call the superclass function that it overrides, and the scope resolution operator is necessary to select the superclass function instead of the subclass function.
class Shape
{
    private:
	int color;					// (a)

    public:
	Shape(int c) : color(c) {}			// (b)

	void draw()					// (c)
	{
		. . . .
	}

	void set_color(int c) { color = c; }		// (d)
};
  1. Shape has a member variable named color, so all Shape subclasses have a color.
  2. The Shape constructor requires an integer representing its color; an initializer list assigns c from the constructor to the color member variable.
  3. Shape defines a draw function that draws a Shape object.
  4. A setter function that assigns c to color. Only constructors may have an initializer list.
class Circle : public Shape				// (e)
{
    private:
	int radius;					// (f)

    public:
	Circle(int c, int r) : Shape(c), radius(r) {}	// (g)

	void draw()					// (h)
	{
		Shape::draw();				// (i)
		. . . .
	}
};
  1. Builds the inheritance relationship; Shape is the superclass and Circle is the subclass.
  2. Circle has a member variable named radius, but not all Shape subclasses do, so it is not appropriate to put radius in Shape.
  3. The Circle constructor has two parameters: the circle's color, c, and radius, r. The initializer list has two elements: a call to the Shape constructor, which must be the first element in the initializer list. The second element assigns parameter r to the the Circle's radius.
  4. A call to the Shape constructor, which must be the first element in the initializer list.
  5. The Circle draw overrides the Shape draw function, and must use the scope resolution operator to call the Shape draw function (rule 3An overriding function in a subclass may (and often does) call the superclass function that it overrides, and the scope resolution operator is necessary to select the superclass function instead of the subclass function.).
Circle c(0xFF00, 5);					// (j)

c.draw();						// (k)

c.Shape::draw();					// (l)

c.set_color(0xFF);					// (m)
  1. Instantiates a Circle object and calls the Circle constructor, which calls the Shape constructor - 0xFF00 is the RGB value for green.
  2. Calls the Circle draw function (rule 2When a subclass object calls an overridden function, it calls the subclass function unless the superclass function is specifically selected.).
  3. Calls the Shape draw function (rule 2When a subclass object calls an overridden function, it calls the subclass function unless the superclass function is specifically selected.).
  4. Circle inherits the set_color function from Shape and can call it directly (rule 1When a subclass object calls an inherited function, a function not overridden by the subclass, it calls the superclass function. In this case, the call is indistinguishable from a call made to a subclass member function.).
Examining inheritance. Circle is a Shape subclass, so instantiating a Circle also instantiates a Shape and the program must initialize both objects. Ellipses denote code omitted for brevity. Elements with the same color refer to the same function, variable, or class. Hover the mouse pointer over the underlined rules for a quick review.

In summary, a superclass may denote a general kind of thing while its subclasses denote more specialized kinds. For example, a Circle and a Rectangle are specialized Shapes. Following this pattern, a superclass function performs general operations while the subclasses override the function to perform more specific operations. For example, the Shape draw function might set the drawing coordinates and the foreground color, and the subclass draw functions draw a circle or rectangle, respectively. The Circle and Rectangle draw functions call the Shape draw function to perform these initial operations and finish by drawing the specific, specialized shapes. Implementing the subclass draw functions this way eliminates duplicating the general code in the Circle and Rectangle draw functions.