Main

March 12, 2004 (Friday)

Chapter 13: Using inheritance and dynamic binding (cont'd)

Using handles to solve the problem (cont'd)

The main() function

Again, we define our main() function below. Note that this main function is almost identical to the original main() function that we saw in earlier lectures. The only change is that the global sort function is passed a static comparison method, as defined in the Student_info class. Because this method is static, there is no need to invoke this method via an object of type Student_info. Instead we use this function simply by using the class name and the scope resolution operator (i.e. Student_info::compare).

#include <algorithm>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

#include "Student_info.h"

using std::cin;			using std::cout;
using std::domain_error;	using std::endl;
using std::setprecision;	using std::setw;
using std::sort;		using std::streamsize;
using std::string;		using std::vector;

using std::max;

int main()
{
	vector<Student_info> students;
	Student_info record;
	string::size_type maxlen = 0;

	// read and store the data
	while (record.read(cin)) {
		maxlen = max(maxlen, record.name().size());
		students.push_back(record);
	}

	// alphabetize the student records
	sort(students.begin(), students.end(), Student_info::compare);

	// write the names and grades
	for (vector<Student_info>::size_type i = 0;
	     i != students.size(); ++i) {
		cout << students[i].name()
		     << string(maxlen + 1 - students[i].name().size(), ' ');
		try {
			double final_grade = students[i].grade();
			streamsize prec = cout.precision();
			cout << setprecision(3) << final_grade
			     << setprecision(prec) << endl;
		} catch (domain_error e) {
			cout << e.what() << endl;
		}
	}
	return 0;
}
main_orig.cpp

Pure virtual functions and abstract base classes (K&M — Chapter 15)

We've discussed the concept of virtual functions during the previous class. Such functions lead to the concept of run-time or dynamic binding, in which the actual function call is not know until run-time. Typically, a base class implements some default behaviour for the virtual function and each derived classes that does not override this function, will inherit the implementation as defined by the base class.

In many practical cases, however, it does not make sense for a base class to implement a virtual function. For example, consider a generic base class called Investment and two derived classes Cash and Stock. We are asked to implement a virtual function which will determine the value of a particular investment. However, for the Investment class, there is no way to calculate its value, because it is too generic or vague — there is no information available in the class from which a value can be calculated. Therefore, rather than implement a empty function for its value() method, we instead use a pure virtual function:

class Investment {
public:
	...
	virtual double value() const = 0;	// Pure virtual function.
	...
};

By assigning 0 to the function declaration, we are saying that it is not possible for the Investment class to determine its value — the Investment class is simply too abstract to allow for a reasonable definition. As a result, because Investment has a pure virtual function, we say that this class is an abstract base class. It is not possible to create objects from an abstract base classes, the compiler will complain. Similarly, objects cannot be created from any derived classes that does not override all the pure virtual functions inherited from an abstract base class. Before a derived class can actually be instantiated, we must override all pure virtual functions as defined in its base class (or ancestor of its base class). The Cash and Stock class can then override this pure virtual function as follows:

class Cash : public Investment {
public:
	double value() const { return amount_ * rate_; }
private:
	double amount_;
	double rate_;
};

class Stock : public Investment {
public:
	double value() const { return num_units_ * stock_price_; }
private:
	double num_units_;
	double stock_price_;
};

Note that we can still create pointers to the abstract base class, but we cannot create objects of classes that contain pure virtual functions:

#include	<iostream>

struct A {
	virtual void hello() = 0;
};

struct B : public A {
	virtual void hello() { std::cout << "I'm B" << std::endl; }
};

struct C : public A {
	virtual void hello() { std::cout << "I'm C" << std::endl; }
};

int
main()
{
	A  a_object	// Illegal!
		
	A *a;		// Okay.

	a = new A;	// Illegal!

	a = new B;	// Okay
	a->hello();	// "I'm B"

	a = new C;	// Okay
	a->hello();	// "I'm C"
}
pvf.cpp

Typically, the pointer to the base class (e.g. A in this case), will be used to point to objects of classes derived from the base class for the purposes of invoking virtual methods via the pointer.

Initializing base classes from derived classes

As mentioned above, we have a base Investment class and two derived classes Cash and Stock. Our constructor for the investment class is fairly straightforward as all we had to do was initialize the name_ data member using the constructor's member initialization list. Remember that the member initialization list is the text in the constructor between the colon and the start of the constructor's body (the body is empty in the case of the Investment constructor):

class Investment {
public:
	Investment (const std::string &n) : name_(n) { }
...
protected:
	std::string	name_;
};

Note the convention of using a underscore suffix on data members of the class. This helps to distinguish them from non-member data values (e.g. method parameters, local variables etc.).

During construction of a derived class, we often want to pass parameters to the base class so that the base portions of the class can be initialized correctly. To do this, we use the member initialization list again. For example, to initialize a Cash object, we make a constructor that takes a string reference (along with details relevant to the Cash class), then we call the Investment constructor in the member initialization list of Cash's constructor, passing the string parameter to it:

class Cash : public Investment {
public:
	Cash (const std::string &n, const double a, const double r)
		: Investment(n), amount_(a), rate_(r) { }
...
private:
	double amount_;
	double rate_;
};

This will correctly initialize the name_ data member in Investment base class. Note that even though the name_ data member is protected inside the base class (and not private), the Cash constructor can not initialize the name_ field directly. This is because the name_ attribute is inherited from the Investment class — it is not an immediate attribute of the Cash class, therefore the Investment class is responsible for initializing it.

Forward declarations

In the context of the investment problem that we were discussing above, we can introduce a Portfolio class to keep track of a collection of investments. In the portfolio.h header file, we use a forward declaration to inform the compiler of the presence of a class called Investment. The syntax of this declaration is quite trivial:

class Investment;

class Portfolio {
public:
	...
private:
	std::vector<Investment*> investments_;
};

Because the Portfolio class uses only a pointer to an Investment there is no need to actually #include the full investment.h header file. Instead, we can simply provide a forward declaration and the compiler will be happy. If portfolio.h had code or method declarations that required a full-fledged Investment object (as opposed to just a pointer or a reference), then we would have to #include the investment.h header file in portfolio.h.

The portfolio.cpp source file, however, could conceivably create Investment objects (or more precisely, objects of classes derived from the Investment class) and would likely call member functions in the Investment hierarchy in order to calculate values such as the overall worth of the portfolio — the portfolio.cpp source file must therefore #include "investment.h". Note that the header files for classes derived from Investment (e.g. stock.h and cash.h) must also #include the investment.h header, but multiple inclusion of the same header file is prevented because of the #ifndef guards at the top of every header file.

The usage of a forward declaration above is pedantic, at best. More practically, forward declarations are required when you are creating mutually referential classes. If class A contains a pointer to class B and class B contains a pointer to class A, then a forward declaration will be necessary in order to break the chicken-and-the-egg problem that such mutually referential classes create:

class B;

class A {
	...
	B *b;
};

class B {
	...
	A *a;
};

Last modified: March 12, 2004 14:13:29 NST (Friday)