|
| |
Linked Lists With Polymorphism
Why More About Linked Lists?
So you thought you'd seen all you ever wanted to know
about linked lists after the last assignment? Read on...
While the previous design worked (at least if you
implemented it right...), it had its drawbacks. Let's list
some of them:
- The list classes were very generic. In particular,
there was no type checking to provide compile-time
checking to prevent mistakes. For example, imagine
you had a list item that represented a Person and
put it into a list, and then you navigated the list
assuming that the list items represented Fruit or a
Wine
-- something other than a Person.
You have to typecast the data from the list
item from a void * to
some other kind of pointer (having to typecast all
the time can become a real pain, and it's very
error-prone). Because there is no type-checking at
compile time, your program will likely compile and
link without problems. There is also no run-time
type-checking, so you will likely get very strange
results when you run your program. Even if you don't
see anything obvious (like an access violation, core
dump, or GPF), there's usually something evil going
on under the surface. In fact, if you don't see
something wrong, that's probably the time to start
worrying, because you don't even know where to start
looking. What's more it's usually a real bear to
debug and fix these kinds of problems, even if you do
see the symptoms.
- There was no support for homogeneous lists -- lists
which can only contain items that represent a single
type. What if you wanted to ensure that all the items
in a list represented Persons?
Or that they all represented Wines?
There is no type-checking of that, at all -- again,
neither at compile-time nor at run-time.
- Even the support for heterogeneous lists wasn't that
great. While you often need to have a list of things
that are of identical type, it's rare that you have a
use for a list that can contain absolutely anything.
More likely, you will have a list of related, or
similar, items. For example, in a payroll
application, you might have a list of Managers, Secretaries,
Programmers,
Janitors
-- yes, even VicePresidents
and the ChairmanOfTheBoard
(yes, even they gets paid...). They are of different
types, but they are all related to each
other -- they are all Employees.
If you wish to ensure that only Employees
(of whatever job description) may be contained in a
list, the previous design doesn't help you much.
- That pesky ListItem
class being separate from the data it represents is a
real pain in the, er... Well, it's very clumsy and
inconvenient. Couldn't we integrate it into the class
that we wish to place in the list?
These shortcomings mostly boil down to lack of safety and
convenience. It's hard enough work to come up a
well-designed, solid and well-tested application; why can't
the compiler help a little more, and then maybe you'd get
home on time at least once a week? Well, it can, but you have
to do some of the work -- more design. An investment in the
proper up-front design can pay great dividends in making it
easier to implement that design, and that can translate into
less work (time, money...) to implement and test that design.
Let's take a look at what we might do to improve things...
Design Alternatives
Here are a couple of ideas on how we might be able to
improve matters:
- Have a ListItem
actually contain, rather than point to, the data it
represents.
- Change every void *
to a specific pointer type. This would allow you to
have a homogeneous list, and would add the necessary
type-checking.
Well, they're fine and dandy as ideas go, but both of them
move us away from the idea that you would like to write the
code for a list once, and then use it for whatever
you wish to place in that list.
For example, let's say that you decided to have a ListItem contain
a Person.
Then you could place the ListItem in a List and (with the
appropriate member functions) manipulate the List with the
full knowledge that it can only contain Persons. With the
proper code, you would also get type-checking, so you could
detect at compile-time when you (or someone who uses your
code) make a stupid mistake.
But what if you also wanted to make a List of Wines? and a List of Fruits? and a List of Employees? Then
you would have to create a PersonListItem,
and a WineListItem,
and a FruitListItem,
and.-- you get the picture... Not only that, to make it all
consistent, you would have create a new List class and a
new ListIterator
class for each type of ListItem
derivative you invent.
Sure, you could simply cut and paste the code to create
new classes each time, with the appropriate changes in types,
but what if someone found a bug in the original
implementation? Could you reliably find all the places you'd
cut and pasted the code to, and fix that bug everywhere it
exists? Multiply that bug by a thousand or more, and the
number of places to find each instance of each bug, and you
might get a feeling for the problem you're creating for
yourself. (It's called the most primitive form of code reuse
-- cut, paste and hope the original doesn't break, and that
you don't introduce new bugs in the copied version as you
make changes.)
Well, this problem is really solved in C++ by template
classes. Unfortunately, there are two problems with
template classes:
- We haven't got to that topic in the course (and may
not; it may turn into a self-study exercise). Be
warned that it's a complex and advanced topic. On the
other hand, the ISO C++ standard includes STL (the
Standard Template Library), a very important generic
programming library, which uses templates up the wazoo (that
means a LOT!). Much
of the C++ standard class library has been redefined
and re-implemented using STL and templates, and so it's a road we
will all have to take eventually.
- Templates have historically been one of the
most troublesome areas of C++. The ISO C++ standards
committee wrestled with them for a long time, and
came up with merely a compromise solution to some of
the problems. Different C++ compilers have
implemented templates in different ways, and with
different features -- indeed, some C++ compilers
don't yet implement them at all, and others don't implement all the
necessary features! So compatibility and
portability across platforms becomes a big issue when
it comes to C++ templates. This will likely improve,
now that the ISO C++ standards committee has finally published the
C++ standard,
but some vendors have been very slow to implement
templates properly. Even if a vendor does implement
templates properly, from the syntactic and semantic
perspectives, that vendor may or may not do a good
job of implementing them efficiently -- templates are
notorious for causing 'code bloat', and you have to
know what the sensible rules are to avoid that.
Phew! That said, what are we going to do in the absence of
templates?
Containment
Well, first, take a look at a technique that I used in the
previous assignment, in the list of Persons.
I'll reproduce it here, so you don't have to flip pages or
browse to it and back:
#ifndef _PERSON
#define _PERSON
// person.h
#include <string.h>
#include "list.h"
class Person
{
public:
Person(char *name);
~Person();
void Add(List &list, Person *prevPerson);
void Remove();
void Print();
operator ListItem &() { return m_item; }
private:
ListItem m_item;
char *m_name;
};
#endif
|
// person.cpp
#include <iostream.h>
#include "person.h"
Person::Person(char *name)
: m_item(this), m_name(new char[strlen(name)+1])
{
strcpy(m_name, name);
}
Person::~Person() { delete [] m_name; }
void Person::Add(List &list, Person *prevPerson)
{
m_item.Add(list, &(prevPerson->m_item));
}
void Person::Remove()
{
m_item.Remove();
}
void Person::Print()
{
cout << "Person: " << m_name << endl;
}
|
// test.cpp
#include "list.h"
#include "person.h"
static void ListPersons()
{
List list;
Person person[] = { Person("Fred"), Person("Mary"), Person("Joe") };
for (int i = 0; i < sizeof(person)/sizeof(person[0]); i++)
{
list.Append(person[i]);
}
ListIterator iter(list);
for (ListItem *item = iter.CurrentItem(); item != 0; item = iter.NextItem())
{
Person *person = (Person *)(item->Data());
person->Print();
}
}
int main()
{
ListPersons();
return 0;
}
|
Note that here, instead of having the ListItem class
contain an instance of class Person,
class Person
contains an instance of class ListItem.
Kind of inside out thinking?
This was a reasonable solution, except for one thorny
problem: How, given an instance of class Person, do I treat
that class like a ListItem,
so it can be placed in the List?
My solution was to introduce into the Person class a conversion
function:
operator ListItem &() { return m_item; }
which allows an instance of class Person to be 'converted' to an instance of class ListItem. In this case, all this means is that the
conversion function returns a reference to m_item, the ListItem instance contained within the Person class. With this, I can write code like:
list.Append(person[i]);
and have the conversion function do its
silent work, making it look like I'm adding a Person to the list. In addition, I added some additional
member functions to class Person:
void Add(List &list, Person *prevPerson);
void Remove();
which allow us to add and remove instances
of class Person from a List, while providing type-safety. Note that these new
member functions are implemented using the existing ListItem member functions. They merely hide the details from
users of the Person class, and also provide the stronger typing..
However, this code does not prevent a
malicious or underachieving programmer from using the bare ListItem class. And we still have the problem of List and ListIterator knowing nothing about the Person class.
We could, of course, apply the same
techniques to the List and ListIterator classes to produce PersonList and PersonIterator classes.
But we won't, because there's a better way
-- using inheritance and polymorphism.
The Assignment (Step 1)
Preparing for Polymorphism
Here's where you get to do some of the work:
First, make a copy of your original linked list classes,
and create a new project to put the copies in. You can always
go back to the old project if you want to have a taste of
something that still works -- we're going to be making some
changes that will initially stop things from working, and you
may get frustrated for a while -- bear with me!
Making the List Classes (pseudo-)Abstract
Now, let's make some changes to the list classes to make
them (sort of) abstract. Remember, an abstract class is one
that cannot be instantiated by itself. For example, you might
choose to make a Fish
class abstract, since it's very generic. If asked to make an
instance of class Fish,
the first question you'd be likely to ask is "What kind
of fish would you like?". Haddock,
or Flounder
would be good, but Fish
is kind of non-specific. Making a class abstract highlights
the fact that you can't really make an instance of that
class, because you need more information.
Conventionally, we make a C++ class abstract by
introducing into that class a pure virtual function.
However, for the moment, at least, I'm not going to do that.
Instead, I'll take a different approach so that I can
introduce some different ideas and techniques.
Note:
We're going to be moving things around inside the classes
to make access to them more restricted. If you look at my
original class definitions, you'll see that I placed the
sections in each class so that public member functions come first,
while private member functions and data
members (remember, data members should always
be private) come last,
at the end. Why? To the clients of a class, the most
important members are its public ones. Ideally, the clients of a
class should have no interest in, and certainly not
depend on, the private parts of a class.
We will be adding a protected section within the class. I
place the protected section between
the public and private sections. I strongly
suggest you follow this example.
Here's what to do:
- In the List
class:
- Move the constructor, List(),
and the member function, void
Append(ListItem &item),
into a protected
section of the class.
- Move the member functions, ListItem *First() const, and ListItem *Last() const, into the private section of
the class.
- Make the destructor virtual.
This should leave only the destructor and the
member functions int
Count() and void
Purge() public
within the List
class.
- In the ListItem
class:
- Move the constructor, ListItem(void
*data), and the member
functions ListItem
*Next() const, ListItem
*Previous() const, and void Add(List
&list, ListItem *prevItem)
into a protected
section of the class.
- Remove the member function void *Data(),
and the data member m_data.
- Change the constructor to ListItem()
-- that is, remove the void *data
argument.
- Make the destructor virtual.
This should leave only the destructor and the
member function void
Remove() public
within the ListItem
class.
- In the ListIterator
class:
- Move the constructor, ListIterator(List
&list), and the member
functions ListItem
*Current(), ListItem *Next(), ListItem
*Previous()
into a protected
section of the class.
- Make the destructor virtual.
This should leave only the destructor and the
member functions bool AtFirst() const, and bool AtLast() const
public within the ListIterator
class.
Now try compiling the result. [Hmmmm.... A few
compile-time errors, huh? Not to worry, it's expected.]
In order to get the list.cpp
file to compile, you may have to add one or two friendship relationships among the
three list classes. You should not have to change any code in
list.cpp to make it compile
-- only the changes I specified above in list.h.
However, you should expect the other .cpp files not
to compile at this point.
For example, the line in file test.cpp:
List list;
will produce errors. Why? Because we made
the constructor for the List
class protected.
The Assignment (Step 2)
Using our Newly (pseudo-)Abstract Classes
So how are we going to use our new List, ListItem, and ListIterator
classes?
Well.we're not, directly. We'll use them indirectly,
by writing modified Wine
and Person
classes. In fact, for each one, we'll write a set of classes,
derived from ListItem,
List, and ListIterator. In
this way, we can create Person-specific and Wine-specific
versions of our List classes, while using inheritance to
ensure that we share the code that implements the lists. At
the same time, we now have classes that are strongly-typed.
I think you'll start to see how this approach can
simplify the code you have to write to use these classes. For
a start, you no longer have to typecast the data into the
proper type; that's done for you by the Person-specific and
Wine-specific classes.
To give you a head start, here is the modified header file
for Person, person.h. Class Person no longer
contains a ListItem
member, but instead inherits (publically!) from ListItem.
#ifndef _PERSON
#define _PERSON
// person.h
#include "list.h"
class PersonList;
// Person
class Person : public ListItem // Note the public!
{
public:
Person(char *name);
Person(const Person &person);
virtual ~Person();
const char *Name() const { return m_name; }
void Add(PersonList &list, Person *prevPerson);
void Print();
private:
char *m_name;
};
// PersonList
class PersonList : public List // Note the public!
{
public:
PersonList() {}
virtual ~PersonList() {}
void Append(Person &person);
};
// PersonIterator
class PersonIterator : public ListIterator // Note the public!
{
public:
PersonIterator(PersonList &list);
virtual ~PersonIterator();
Person *Current();
Person *Next();
Person *Previous();
};
#endif
|
Note that I have removed all traces of List, ListItem, or ListIterator from
the public interfaces of these classes. The intent is to
implement all the functionality of the Person classes
using the functionality provided by those underlying list
classes, without exposing that fact to the outside world..
Here's What to Do:
Your tasks are to:
- Implement class Person, in person.cpp.
- Implement a corresponding set of
classes for class Wine, in wine.h and wine.cpp.
- Modify the test program in test.cpp as follows:
- Combine the ListPersons() and ListWines() functions into a single file, and:
- Augment the main() function of the test program to
call each of these functions.
The resulting main function should look
like:
// ...
int main()
{
ListPersons();
ListWines();
return 0;
}
|
Note:
Of course, the program should work correctly. So test
it, and show me the results.
Warning:
Do not
simply copy the code from List, ListItem, and
ListIterator into the Person and Wine classes! This
would be to miss the whole point of the exercise! You
would not be benefiting from the code reuse you get from a single implementation
of the code!
The Assignment (Step 3)
Supporting Heterogeneous Lists of Related Items
The above changes improved matters somewhat:
- Homogeneous lists are now easier to implement (if a
little repetitious)
- We now have strong typing, and no need for
typecasting in our main code.
However, we don't yet have support for the kind of
heterogeneous list that contains items which are not all
identical types, but which are related to each other (as in
the Employees example, earlier). So let's see what we can do
to make this possible:
First, let's come up with a set of related classes: Employee, Manager, and Secretary. Note
that Employee
is derived from Person,
and has related classes EmployeeList
and EmployeeIterator,
while Manager
and Secretary
are simply derived from Employee,
and do not have their own list or iterator. With this
infrastructure, we can support a heterogeneous list of items
which are related to each other: they are all Employees.
#ifndef _EMPLOYEE
#define _EMPLOYEE
// employee.h
#include "person.h"
class EmployeeList;
// Employee
class Employee : public Person
{
public:
Employee(char *name, double salary);
Employee(const Employee &emp);
~Employee();
void Add(EmployeeList &list, Employee *prevEmployee);
void Remove();
void Print();
private:
double m_salary;
};
// EmployeeList
class EmployeeList : public PersonList
{
public:
EmployeeList();
virtual ~EmployeeList();
void Append(Employee &emp);
};
// EmployeeIterator
class EmployeeIterator : public PersonIterator
{
public:
EmployeeIterator(EmployeeList &list);
virtual ~EmployeeIterator();
Employee *Current();
Employee *Next();
Employee *Previous();
};
////////// Other types of Employees ///////////////
class Secretary;
// Manager
class Manager : public Employee
{
public:
Manager(char *name, double salary);
virtual ~Manager();
Secretary *GetSecretary();
void SetSecretary(Secretary *sec);
void Print();
private:
// Don't allow copying or assignment of Managers
Manager(const Manager &);
Manager &operator=(const Manager &);
//// Data //////
Secretary *m_secretary;
};
// Secretary
class Secretary : public Employee
{
public:
Secretary(char *name, double salary);
virtual ~Secretary();
Manager *GetManager();
void SetManager(Manager *sec);
void Print();
private:
// Don't allow copying or assignment of Secretaries
Secretary(const Secretary &);
Secretary &operator=(const Secretary &);
//// Data //////
Manager *m_manager;
};
#endif
|
Here's What to Do:
Now implement these classes (in employee.h
and employee.cpp). Creating Employee, EmployeeList, and EmployeeIterator
is just like what you did before to construct Person, PersonList and PersonIterator,
except that you derive Employee,
et.al. from Person,
et.al, rather than List,
et.al.
Be sure to make each class' Print()
function do something different. For example, Employee::Print()
should pring out the Person
information (using Person::Print())
and then print out the salary, as well. Manager::Print()
should print out the Employee
information (using Employee.Print()),
and then print out the secretary name, if any. Secretary::Print()
should print out the Employee
information, and then print out the boss' name, if any.
Question:
When you iterate through such a list, do you see
the wrong Print() function being called for some
items? What do you need to do to make sure that
the proper Print() function is called for each
item?
Here's a function to test out your new functionality:
static void ListEmployees()
{
EmployeeList list;
Employee FredBloggs("Fred Bloggs", 25000);
Manager GenghisKhan("Genghis Khan", 105000);
Secretary MargaretThatcher("Margaret Thatcher", 30000);
Manager BillGates("Bill Gates", 42000000);
Employee NewtGingrich("Newt Gingrich", 89000);
Secretary Madonna("Madonna", 5000000);
Employee *employee[] =
{
&FredBloggs, &GenghisKhan, &MargaretThatcher,
&BillGates, &NewtGingrich, &Madonna
};
GenghisKhan.SetSecretary(&MargaretThatcher);
BillGates.SetSecretary(&Madonna);
for (int i = 0; i < sizeof(employee)/sizeof(employee[0]); i++)
{
list.Append(*(employee[i]));
}
EmployeeIterator iter(list);
for (Employee *e = iter.Current(); e != 0; e = iter.Next())
{
e->Print();
}
}
|
Add a call in the main function to call it, so you can
test with it.
Note that this function constructs a list of items that
are all Employees, or derived from Employees.
|