Inheritance, part 2

Base and Derived relationships
- Base and derived classes have a special relationship

- Derived objects can be assigned to base objects without explicit casting
- A derived object address can be assigned to a base object pointer without explicit casting

- Let’s look at an example using the following class hierarchy

 

 

 

// Vehicle class

class Vehicle

{

protected:

  float distance;    // in miles

  float speed;       // in miles per hour

 

public:

  // Constructor

  Vehicle() { distance = 0.0; speed = 0.0; }

 

  // Compute time of travel

  float computeDuration();

 

  // Access methods

  float getSpeed() { return speed; }

  void setSpeed(float s) { speed = s; }

  float getDistance() { return distance; }

  void setDistance(float d) { distance = d; }

};

float Vehicle::computeDuration()

{

  return ( distance / speed );

}

 

// Wheeled vehicle

class WheelVehicle : public Vehicle

{

protected:

  int wheels;        // number of wheels

 

public:

  // Constructor

  WheelVehicle() { wheels = 0; }

 

  // Access methods

  void setWheels(int w) { wheels = w; }

  int getWheels() { return wheels; }

};

 

// Truck class

class Truck : public WheelVehicle

{

private:

  float carryingLoad;

 

public:

  Truck() { carryingLoad = 0.0; }

 

  // Access methods

  void setLoad( float l ) { carryingLoad = l; }

  float getLoad() { return carryingLoad; }

};

 

 

#include <iostream>

 

using namespace std;

 

main()

{

  Truck semi;

  semi.setLoad(2);

  semi.setWheels(18);

  semi.setSpeed(55.0);

  semi.setDistance(300.0);

  cout << "Duration for truck: " << semi.computeDuration() << endl;

 

  // Derived class assigned to parent class

  WheelVehicle wv = semi;

  cout << "Duration for wv: " << wv.computeDuration() << endl;

 

  // Derived class assigned to parent of parent class

  Vehicle v = semi;

  cout << "Duration for v: " << v.computeDuration() << endl;

 

  // Derived class address assigned to base class pointer

  Vehicle *vp = &semi;

  cout << "Duration for vp: " << vp->computeDuration() << endl;

}

 

Output

Duration for truck: 5.45455

Duration for wv: 5.45455

Duration for v: 5.45455

Duration for vp: 5.45455

 

 

- Note that a derived object can be assigned to class object higher up on the hierarchy

- In our example we assign a Truck object to both a WheelVehicle and a Vehicle object

 

WheelVehicle wv = semi;

Vehicle v = semi;

 

- Note also that we can assign the address of a derived object to a class object pointer higher on the hierarchy

- Specifically in our example, we assign the address of a Truck object to a base Vehicle object pointer

- Note how all these assignments do not require any type casting upon assignment

 

Vehicle *vp = &semi;

 

- This relationship can be very useful and provide a general type of programming

- Consider, for example, if we needed to store a whole fleet of different types of vehicles

- Rather than storing each class separately, we could store each using base class pointers

- We might consider storing the address of each different vehicle type in an STL vector

 

            vector<Vehicle *> vehicles;

 

- With this vector of base class pointers, we can easily add and store new vehicles in our fleet

 

vehicles.push_back( new Truck );

      vehicles.push_back( new Auto );

      ...

           

- With such a structure, we can loop through each base class pointer and perform a task on each vehicle

- Suppose this task was to compute the time of travel for each vehicle

- Up till now, the time of travel for each type of vehicle in the hierarchy has been the same

- This is because the method, computeDuration, is in the general base Vehicle class

- Different vehicles, however, might have different travel times depending on a number of factors

- Wheeled vehicles, for example, might be slowed down from road construction

- The carrying load for a truck, for example, would further slow down the travel time

- We need each type in the hierarchy to have its own way of computing travel time

- In other words, we need different classes in the hierarchy to have different meanings

- Let’s add such methods to the WheelVehicle and the Truck class

 

// New method of WheelVehicle class

float computeDuration()

{

    float tmp = Vehicle::computeDuration();

    float road_construction = 1.0;

    return (tmp + road_construction);

}

 

// New method of Truck class

float computeDuration()

{

    float tmp = WheelVehicle::computeDuration();

    return (tmp + carryingLoad);

}

 

- Note how the methods are named identically in the hierarchy

- Note also how we’re adding to the travel time computed from the previous class in the hierarchy

- We do this by calling the parent class method with the scoping operator

- Let’s create a fleet of vehicles with base class pointers and compute the time of travel for each

 

#include <iostream>

#include <vector>

 

using namespace std;

 

main()

{

  WheelVehicle v1;

  v1.setSpeed(55.0);

  v1.setDistance(300.0);

  v1.setWheels(4);

  cout << "v1 (wheeled vehicle): " << v1.computeDuration() << endl;

 

  Truck v2;

  v2.setSpeed(55.0);

  v2.setDistance(300.0);

  v2.setWheels(18);

  v2.setLoad(2);

  cout << "v2 (truck): " << v2.computeDuration() << endl;

 

  vector< Vehicle *> vehicles;

  vehicles.push_back( &v1 );

  vehicles.push_back( &v2 );

  for( int i = 0; i < vehicles.size(); ++i )

  {

    cout << "Vehicle " << i << ": " << vehicles[i]->computeDuration();

    cout << endl;

  }

}

 

Output

v1 (wheeled vehicle): 6.45455

v2 (truck): 8.45455

Vehicle 0: 5.45455

Vehicle 1: 5.45455

 

 

- Look closely at the durations (times of travel) printed in the output

- The first base Vehicle pointer calls the computeDuration method from the Vehicle class

- This is occurring even though it is actually storing the address of a WheelVehicle object (v1)

- Likewise, the second base Vehicle pointer also calls the Vehicle computeDuration method

- This is occurring even though it is actually storing the address of a Truck object (v2)

- How can we get the base pointers to call the methods of the objects they are pointing to?

- A first attempt might look something like this (pseudo-code example)

 

#define WHEELV 1

#define TRUCK 2

 

add

int whatIsMyType() { return (WHEELV, TRUCK) }

to every class

 

void printDuration( Vehicle *v )

{

  switch( v->whatIsMyType() )

    {

    case WHEELV:

      WheelVehicle *wv = (WheelVehicle *)v;

      wv->computeDuration();

      break;

 

    case TRUCK:

      Truck *t = (Truck *)v;

      t->computeDuration();

      break;

    }

}

- Although this method would work, this solution does not scale well if we added new vehicle classes

- For every new vehicle type, we would have to add another case statement in the solution above

- We would have to add and modify code dependent on a new type in the hierarchy

- In large applications with large class hierarchies, such “type resolution” can be quite burdensome

- We need a better and more efficient method of resolving different types in applications

- A better solution is to use dynamic binding with virtual functions

   

 

 

Dynamic Binding & Virtual Functions 

- Dynamic binding allows us to call derived class methods using base class pointers

- The “type resolution” is determined at run-time and works with pointers to objects only

- No need for programmer to determine type as in the pseudo-code example above
- Reduces size and complexity of code and makes code more extensible
- To implement dynamic binding, we need a new mechanism called a virtual function


 

Virtual Function

- Special type of function used in class hierarchies

- Precede function declaration with keyword virtual
- Keyword only needs to be in method of base class, optional in derived classes

virtual <return type> <name> (<arg1, arg2, ...>)

- When methods are virtual, derived class methods override the base class methods
- A better term would be "adaptable" rather than virtual
- The overridden method in derived class is adapted to better fit the nature of the class
- Functions should be virtual in a class when
        1) The class expects to be an object of derivation
        2) The implementation of the function is type-dependent

- Allows derived objects to respond to the same command (i.e. computeDuration) in different ways
- This object-oriented feature is also called polymorphism
- Polymorphism is a fundamental advantage of object-oriented programming
- Dynamic binding will occur with the class that first declares a method
virtual
- Base and derived methods must have same parameters and types (same declaration)

- Let’s look at an example of dynamic binding with a virtual function

- Consider a general base class describing an error message

- A derived class describes a more specific type of syntax error message

 

#include <iostream>

 

using namespace std;

 

// Base class

class error_msg

{

public:

  virtual void display()

  {

    cout << "An error has occurred" << endl;

  }

};

 

// Derived class

class syntax_msg : public error_msg

{

public:

  void display()

  {

    cout << "Semicolon missing in statement" << endl;

  }

};

 

main()

{

  error_msg *m[2];            // array of object pointers

  error_msg msg;              // error message

  syntax_msg smsg;            // syntax error message

  m[0] = &msg;

  m[1] = &smsg;

  m[0]->display();

  m[1]->display();

}

 

Output

An error has occurred

Semicolon missing in statement

 

 

- Note how the method display is declared to be virtual

- Note how dynamic binding works with the base class pointers (m[0] and m[1])

- They correctly call the method of the type of object they are pointing to

- A derived class describes a more specific type of syntax error message

- What happens if we don’t include the virtual keyword?

- Without the virtual keyword, base class pointers will call base class methods

- Dynamic binding will not occur (as in the example below)

- Dynamic binding works only with pointers and virtual methods

 

#include <iostream>

 

using namespace std;

 

// Base class

class error_msg

{

public:

 

  // Note no virtual keyword

  void display()

  {

    cout << "An error has occurred" << endl;

  }

};

 

// Derived class

class syntax_msg : public error_msg

{

public:

  void display()

  {

    cout << "Semicolon missing in statement" << endl;

  }

};

 

main()

{

  error_msg *m[2];            // array of object pointers

  error_msg msg;              // error message

  syntax_msg smsg;            // syntax error message

  m[0] = &msg;

  m[1] = &smsg;

  m[0]->display();

  m[1]->display();

}

 

Output

An error has occurred

An error has occurred

 

 

Final Example
- Let's put dynamic binding to work with our original vehicle example

- In our previous example, the correct computeDuration method was not being called

- We now know that for this to occur, we need to make this function virtual

- Below is our final class hierarchy with a virtual method to compute the time of travel

- We also include our original example that now prints the correct computed times of travel

 

// Vehicle class

class Vehicle

{

protected:

  float distance;    // in miles

  float speed;       // in miles per hour

 

public:

  // Constructor

  Vehicle() { distance = 0.0; speed = 0.0; }

 

  // Compute time of travel

  virtual float computeDuration();

 

  // Access methods

  float getSpeed() { return speed; }

  void setSpeed(float s) { speed = s; }

  float getDistance() { return distance; }

  void setDistance(float d) { distance = d; }

};

float Vehicle::computeDuration()

{

  return ( distance / speed );

}

 

// Wheeled vehicle

class WheelVehicle : public Vehicle

{

protected:

  int wheels;        // number of wheels

 

public:

  // Constructor

  WheelVehicle() { wheels = 0; }

 

  // Compute duration

  float computeDuration()

  {

    float tmp = Vehicle::computeDuration();

    float road_construction = 1.0;

    return (tmp + road_construction);

  }

 

 

  // Access methods

  void setWheels(int w) { wheels = w; }

  int getWheels() { return wheels; }

};

 

// Truck class

class Truck : public WheelVehicle

{

private:

  float carryingLoad;

 

public:

  Truck() { carryingLoad = 0.0; }

 

  // Compute duration

  float computeDuration()

  {

    float tmp = WheelVehicle::computeDuration();

    return (tmp + carryingLoad);

  }

 

  // Access methods

  void setLoad( float l ) { carryingLoad = l; }

  float getLoad() { return carryingLoad; }

};

 

 

#include <iostream>

#include <vector>

 

using namespace std;

 

main()

{

  WheelVehicle v1;

  v1.setSpeed(55.0);

  v1.setDistance(300.0);

  v1.setWheels(4);

  cout << "v1 (wheeled vehicle): " << v1.computeDuration() << endl;

 

  Truck v2;

  v2.setSpeed(55.0);

  v2.setDistance(300.0);

  v2.setWheels(18);

  v2.setLoad(2);

  cout << "v2 (truck): " << v2.computeDuration() << endl;

 

  vector< Vehicle *> vehicles;

  vehicles.push_back( &v1 );

  vehicles.push_back( &v2 );

  for( int i = 0; i < vehicles.size(); ++i )

    cout << "Vehicle " << i << ": " << vehicles[i]->computeDuration() << endl;

}

 

Output

v1 (wheeled vehicle): 6.45455

v2 (truck): 8.45455

Vehicle 0: 6.45455

Vehicle 1: 8.45455