Inheritance - C++ Recipes: A Problem-Solution Approach (2015)

C++ Recipes: A Problem-Solution Approach (2015)

CHAPTER 6

image

Inheritance

C++ allows you to build complex software applications in a number of ways. One of the most common is the object-oriented programming (OOP) paradigm. Classes in C++ are used to provide a blueprint for objects that contain your data and the operations that can be carried out on that data.

Inheritance takes this a step further by letting you construct complex hierarchies of classes. The C++ language provides various different features you can use to organize your code in a logical manner.

Recipe 6-1. Inheriting from a Class

Problem

You’re writing a program that has a natural is-a relationship between objects and would like to reduce code duplication.

Solution

Inheriting a class from a parent class allows you to add your code to the parent and share it between multiple derived types.

How It Works

In C++, you can inherit one class from another. The inheriting class gains all the properties of the base class. Listing 6-1 shows an example of two classes that inherit from a shared parent.

Listing 6-1. Class Inheritance

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
private:
uint32_t m_NumberOfWheels{};

public:
Vehicle(uint32_t numberOfWheels)
: m_NumberOfWheels{ numberOfWheels }
{

}

uint32_t GetNumberOfWheels() const
{
return m_NumberOfWheels;
}
};

class Car : public Vehicle
{
public:
Car()
: Vehicle(4)
{

}
};

class Motorcycle : public Vehicle
{
public:
Motorcycle()
: Vehicle(2)
{

}
};

int main(int argc, char* argv[])
{
Car myCar{};
cout << "A car has " << myCar.GetNumberOfWheels() << " wheels." << endl;

Motorcycle myMotorcycle;
cout << "A motorcycle has " << myMotorcycle.GetNumberOfWheels() << " wheels." << endl;

return 0;
}

The Vehicle class contains a member variable to store the number of wheels the vehicle has. This value is initialized to 0 by default or is set in the constructor. Vehicle is followed by another class named Car. The Car class contains only a constructor that is used to call the constructor for Vehicle. The Car constructor passes the number 4 into the Vehicle constructor and therefore sets m_NumberOfWheels to 4.

The Motorcycle class also contains only a constructor, but it passes 2 to the Vehicle constructor. Because both Car and Motorcycle inherit from the Vehicle class, they both inherit its properties. They both contain a variable to hold the number of wheels, and they will both have a method to retrieve the number of wheels. You can see this in the main function, where GetNumberOfWheels is called on both the myCar object and the myMotorcycle object. Figure 6-1 shows the output generated by this code.

9781484201589_Fig06-01.jpg

Figure 6-1. Output generated by the code in Listing 6-1

The Car class and the Motorcycle class both inherit the properties of Vehicle and both set the appropriate number of wheels in their constructor.

Recipe 6-2. Controlling Access to Member Variables and Methods in Derived Classes

Problem

Your derived class needs to be able to access the fields in its parent.

Solution

C++ access modifiers have an effect on the way variables can be accessed in derived classes. Using the correct access modifier is essential in properly constructing a class hierarchy.

How It Works

The public Access Specifier

The public access specifier grants public access to a variable or method in a class. This applies equally to member variables and methods. You can see this clearly in Listing 6-2.

Listing 6-2. The public Access Specifier

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
public:
uint32_t m_NumberOfWheels{};

Vehicle() = default;
};

class Car : public Vehicle
{
public:
Car()
{
m_NumberOfWheels = 4;
}
};

class Motorcycle : public Vehicle
{
public:
Motorcycle()
{
m_NumberOfWheels = 2;
}
};

int main(int argc, char* argv[])
{
Car myCar{};
cout << "A car has " << myCar.m_NumberOfWheels << " wheels." << endl;
myCar.m_NumberOfWheels = 3;
cout << "A car has " << myCar.m_NumberOfWheels << " wheels." << endl;

Motorcycle myMotorcycle;
cout << "A motorcycle has " << myMotorcycle.m_NumberOfWheels << " wheels." << endl;
myMotorcycle.m_NumberOfWheels = 3;
cout << "A motorcycle has " << myMotorcycle.m_NumberOfWheels << " wheels." << endl;

return 0;
}

Any variables with public access can be accessed by a derived class. Both the Car constructor and the Motorcycle constructor take advantage of this and set the number of wheels they have appropriately. The downside is that other code can also access the public member variables. You can see this in the main function, where the m_NumberOfWheels is read and assigned to both the myCar object and the myMotorcycle object. Figure 6-2 shows the output generated by this code.

9781484201589_Fig06-02.jpg

Figure 6-2. The output generated by Listing 6-2

The private Access Specifier

Instead of making variables public, you can make them private and provide public accessors to them. Listing 6-3 shows the use of a private member variable.

Listing 6-3. The private Access Specifier

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
private:
uint32_t m_NumberOfWheels{};

public:
Vehicle(uint32_t numberOfWheels)
: m_NumberOfWheels{ numberOfWheels }
{

}

uint32_t GetNumberOfWheels() const
{
return m_NumberOfWheels;
}
};

class Car : public Vehicle
{
public:
Car()
: Vehicle(4)
{

}
};

class Motorcycle : public Vehicle
{
public:
Motorcycle()
: Vehicle(2)
{

}
};

int main(int argc, char* argv[])
{
Car myCar{};
cout << "A car has " << myCar.GetNumberOfWheels() << " wheels." << endl;

Motorcycle myMotorcycle;
cout << "A motorcycle has " << myMotorcycle.GetNumberOfWheels() << " wheels." << endl;

return 0;
}

Listing 6-3 shows the use of the private access specifier with the m_NumberOfWheels variable. The Car and Motorcycle classes can no longer access the m_NumberOfWheels variable directly; therefore, the Vehicle class provides a method to initialize the variable through its constructor. This makes the classes a little harder to work with but adds the benefit of not allowing any external code direct access to the member variable. You ca see this in the main function, where the code must get the number of wheels through the GetNumberOfWheels accessor method.

The protected Access Specifier

The protected access specifier allows for a mix of public and private access specifiers. It acts like a public specifier for classes that derive from the current class, and it acts like a private specifier for external code. Listing 6-4 shows this behavior.

Listing 6-4. The protected Access Specifier

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
protected:
uint32_t m_NumberOfWheels{};

public:
Vehicle() = default;

uint32_t GetNumberOfWheels() const
{
return m_NumberOfWheels;
}
};

class Car : public Vehicle
{
public:
Car()
{
m_NumberOfWheels = 4;
}
};

class Motorcycle : public Vehicle
{
public:
Motorcycle()
{
m_NumberOfWheels = 2;
}
};

int main(int argc, char* argv[])
{
Car myCar{};
cout << "A car has " << myCar.GetNumberOfWheels() << " wheels." << endl;

Motorcycle myMotorcycle;
cout << "A motorcycle has " << myMotorcycle.GetNumberOfWheels() << " wheels." << endl;

return 0;
}

Listing 6-4 shows that both Car and Motorcycle can access the m_NumberOfWheels variable directly from their parent class, Vehicle. Both classes set the m_NumberOfWheels variable in their constructors. The calling code in the main function doesn’t have access to this variable and therefore has to call the GetNumberOfWheels method to be able to print this value.

Recipe 6-3. Hiding Methods in Derived Classes

Problem

You have a derived class that needs behavior in a method that is different than the behavior provided by the parent class.

Solution

C++ allows you to hide methods in parent classes by defining a method with the same signature in the derived class.

How It Works

You can hide a method in a parent class by defining a method with exactly the same signature in the base class. This example shows how derived classes can use explicit method hiding to provide functionality that differs from the parent class’s. This is a key concept to understand when you’re using inheritance, because it’s the primary method employed to differentiate hierarchies of class types.

Listing 6-5 contains a Vehicle class, a Car class, and a Motorcycle class. The Vehicle class defines a method named GetNumberOfWheels that returns 0. The same method is defined in the Car class and the Motorcycle class; these versions of the method return 4 and 2, respectively.

Listing 6-5. Hiding Methods

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
public:
Vehicle() = default;

uint32_t GetNumberOfWheels() const
{
return 0;
}
};

class Car : public Vehicle
{
public:
Car() = default;

uint32_t GetNumberOfWheels() const
{
return 4;
}
};

class Motorcycle : public Vehicle
{
public:
Motorcycle() = default;

uint32_t GetNumberOfWheels() const
{
return 2;
}
};

int main(int argc, char* argv[])
{
Vehicle myVehicle{};
cout << "A vehicle has " << myVehicle.GetNumberOfWheels() << " wheels." << endl;

Car myCar{};
cout << "A car has " << myCar.GetNumberOfWheels() << " wheels." << endl;

Motorcycle myMotorcycle;
cout << "A motorcycle has " << myMotorcycle.GetNumberOfWheels() << " wheels." << endl;

return 0;
}

The main function in Listing 6-5 calls the three different versions of GetNumberOfWheels and returns the appropriate value for each. You can see the output generated by this code in Figure 6-3.

9781484201589_Fig06-03.jpg

Figure 6-3. The output generated by executing the code in Listing 6-5

Accessing these methods directly through objects or pointers to these class types results in the correct output.

Image Note Method hiding doesn’t work properly when you’re using polymorphism. Accessing a derived class through a pointer to a base class results in the method on the base class being called. This is very rarely the behavior you want. See Recipe 8-5 for the proper solution when using polymorphism.

Recipe 6-4. Using Polymorphic Base Classes

Problem

You would like to write generic code that works with pointers to base classes and that still calls the proper methods in derived classes.

Solution

The virtual keyword allows you to create methods that can be overridden by derived classes.

How It Works

The virtual keyword tells the C++ compiler that you would like a class to contain a virtual method table (v-table). A v-table contains lookups for methods that allows the correct method to be called for a given type even if the object is being accessed through a pointer to one of its parent classes. Listing 6-6 shows a class hierarchy that uses the virtual keyword to specify that a method should be included in the class’s v-table.

Listing 6-6. Creating a Virtual Method

#include <cinttypes>

class Vehicle
{
public:
Vehicle() = default;

virtual uint32_t GetNumberOfWheels() const
{
return 2;
}
};

class Car : public Vehicle
{
public:
Car() = default;

uint32_t GetNumberOfWheels() const override
{
return 4;
}
};

class Motorcycle : public Vehicle
{
public:
Motorcycle() = default;
};

The Car and Motorcycle classes in Listing 6-6 derive from the Vehicle class. The GetNumberOfWheels method in the Vehicle class is listed as a virtual method. This causes any calls to that method through a pointer to be called through the v-table. Listing 6-7 shows a full example with a main function that accesses objects through a Vehicle pointer.

Listing 6-7. Accessing Virtual Methods through a Base Pointer

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
public:
Vehicle() = default;

virtual uint32_t GetNumberOfWheels() const
{
return 2;
}
};

class Car : public Vehicle
{
public:
Car() = default;

uint32_t GetNumberOfWheels() const override
{
return 4;
}
};

class Motorcycle : public Vehicle
{
public:
Motorcycle() = default;
};

int main(int argc, char* argv[])
{
Vehicle* pVehicle{};

Vehicle myVehicle{};
pVehicle = &myVehicle;
cout << "A vehicle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

Car myCar{};
pVehicle = &myCar;
cout << "A car has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

Motorcycle myMotorcycle;
pVehicle = &myMotorcycle;
cout << "A motorcycle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

return 0;
}

The main function defines a pointer to a Vehicle object on its first line. This pointer is then used in each of the cout statements to access the GetNumberOfWheels method for the current object. The Vehicle and Motorcycle objects have the address of theVehicle::GetNumberOfWheels method in their v-tables; therefore, both return 2 for their number of wheels.

The Car class overrides the GetNumberOfWheels method. This causes Car to replace the address for Vehicle::GetNumberOfWheels in the lookup table with the address of Car::GetNumberOfWheels. As a result, when the same Vehicle pointer is assigned the address of myCar and subsequently calls GetNumberOfWheels, it calls the method defined in the Car class and not that defined in the Vehicle class. Figure 6-4 shows the output generated by the code in Listing 6-7, where you can see that this is the case.

9781484201589_Fig06-04.jpg

Figure 6-4. The output generated by executing the code in Listing 6-7

The override keyword is used at the end of the GetNumberOfWheels method’s signature in the Car class. This keyword is a hint to the compiler that you expect this method to override a virtual method in the parent class. The compiler will throw an error if you enter the signature incorrectly or if the signature of the method you’re overriding is changed later. This feature is very useful, and I recommend that you use it (although the override keyword itself is optional).

Recipe 6-5. Preventing Method Overrides

Problem

You have a method that you don’t wish to be overridden by deriving classes.

Solution

You can use the final keyword to prevent classes from overriding a method.

How It Works

The final keyword informs the compiler that you don’t want a virtual method to be overridden by a deriving class. Listing 6-8 shows an example of using the final keyword.

Listing 6-8. Using the final Keyword

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
public:
Vehicle() = default;

virtual uint32_t GetNumberOfWheels() const final
{
return 2;
}
};

class Car : public Vehicle
{
public:
Car() = default;

uint32_t GetNumberOfWheels() const override
{
return 4;
}
};

class Motorcycle : public Vehicle
{
public:
Motorcycle() = default;
};

int main(int argc, char* argv[])
{
Vehicle* pVehicle{};

Vehicle myVehicle{};
pVehicle = &myVehicle;
cout << "A vehicle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

Car myCar{};
pVehicle = &myCar;
cout << "A car has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

Motorcycle myMotorcycle;
pVehicle = &myMotorcycle;
cout << "A motorcycle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

return 0;
}

The GetNumberOfWheels method in the Vehicle class uses the final keyword to prevent derived classes from trying to override it. This causes the code in Listing 6-8 to fail to compile, because the Car class attempts to override GetNumberOfWheels. You can comment out this method to get the code to compile.

The final keyword can also stop further overrides of a method in a longer chain. Listing 6-9 shows how this is possible.

Listing 6-9. Preventing Overrides in an Inheritance Hierarchy

#include <cinttypes>

class Vehicle
{
public:
Vehicle() = default;

virtual uint32_t GetNumberOfWheels() const
{
return 2;
}
};

class Car : public Vehicle
{
public:
Car() = default;

uint32_t GetNumberOfWheels() const final
{
return 4;
}
};

class Ferrari : public Car
{
public:
Ferrari() = default;

uint32_t GetNumberOfWheels() const override
{
return 5;
}
};

Vehicle defines a virtual method named GetNumberOfWheels that returns the value 2. Car overrides this method to return 4 (this example ignores the fact that not all cars have four wheels) and declares that the method is final. No other classes deriving from Car are allowed to override the same method. This makes sense for the application if the requirements only require support for four-wheeled cars. The compiler will throw an error when it reaches any class that derives from Car or derives from any other class that has Car in its hierarchy and that tries to override the GetNumberOfWheels method.

Recipe 6-6. Creating Interfaces

Problem

You have a base class method that should not define any behavior but should simply be overridden by deriving classes.

Solution

You can create pure virtual methods in C++ that don’t define a method body.

How It Works

You can define pure virtual methods in C++ by adding = 0 to the end of the method signature. Listing 6-10 shows an example.

Listing 6-10. Creating Pure Virtual Methods

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
public:
Vehicle() = default;

virtual uint32_t GetNumberOfWheels() const = 0;
};

class Car : public Vehicle
{
public:
Car() = default;

uint32_t GetNumberOfWheels() const override
{
return 4;
}
};

class Motorcycle : public Vehicle
{
public:
Motorcycle() = default;

uint32_t GetNumberOfWheels() const override
{
return 2;
}
};

int main(int argc, char* argv[])
{
Vehicle* pVehicle{};

Car myCar{};
pVehicle = &myCar;
cout << "A car has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

Motorcycle myMotorcycle;
pVehicle = &myMotorcycle;
cout << "A motorcycle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

return 0;
}

The Vehicle class defines GetNumberOfWheels as a pure virtual method. This has the effect of ensuring that an object of type Vehicle can never be created. The compiler doesn’t allow this because it doesn’t have a method to call for GetNumberOfWheels. Car andMotorcycle both override this method and can be instantiated. You can see this occur in the main function. Figure 6-5 shows that the methods return the correct values for Car and Motorcycle.

9781484201589_Fig06-05.jpg

Figure 6-5. The output generated by executing the code in Listing 6-10

A class that contains a pure virtual method is known as an interface. If a class inherits from an interface and you wish to be able to instantiate that class, you must override any pure virtual methods in the parent. It’s possible to derive from an interface and not override these methods, but that derived class can then only be used as an interface to further derived classes.

Recipe 6-7. Multiple Inheritance

Problem

You have a class that you wish to derive from more than one parent.

Solution

C++ supports multiple inheritance.

How It Works

You can derive a class from multiple parents in C++ using a comma-separated list of parent classes. Listing 6-11 shows how this can be achieved.

Listing 6-11. Multiple Inheritance

#include <cinttypes>
#include <iostream>

using namespace std;

class Printable
{
public:
virtual void Print() = 0;
};

class Vehicle
{
public:
Vehicle() = default;

virtual uint32_t GetNumberOfWheels() const = 0;
};

class Car
: public Vehicle
, public Printable
{
public:
Car() = default;

uint32_t GetNumberOfWheels() const override
{
return 4;
}

void Print() override
{
cout << "A car has " << GetNumberOfWheels() << " wheels." << endl;
}
};

class Motorcycle
: public Vehicle
, public Printable
{
public:
Motorcycle() = default;

uint32_t GetNumberOfWheels() const override
{
return 2;
}

void Print() override
{
cout << "A motorcycle has " << GetNumberOfWheels() << " wheels." << endl;
}
};

int main(int argc, char* argv[])
{
Printable* pPrintable{};

Car myCar{};
pPrintable = &myCar;
pPrintable->Print();

Motorcycle myMotorcycle;
pPrintable = &myMotorcycle;
pPrintable->Print();

return 0;
}

The Car and Motorcycle classes both derive from multiple parents. These classes are now both Vehicles and Printables. You can see the interplay between the two parents in the overridden Print methods. These methods both call the overridden GetNumberOfWheelsmethod in Car and Motorcycle. The main function accesses the overridden Print methods through a pointer to a Printable object, using polymorphism to call the correct Print method and also the correct GetNumberOfWheels method in Print. Figure 6-6 shows that the output from the program is correct.

9781484201589_Fig06-06.jpg

Figure 6-6. Output showing that multiple inheritance works with polymorphism