Inapoi
Inainte
Cuprins
Inheritance and composition
provide a way to reuse object code. The
template feature in C++
provides
a way to reuse source
code.
Although C++ templates are a
general-purpose programming tool, when they were introduced in the language,
they seemed to discourage the use of object-based container-class hierarchies
(demonstrated at the end of Chapter 15). For example, the Standard C++
containers and algorithms (explained in two chapters of Volume 2 of this book,
downloadable from www.BruceEckel.com) are built exclusively with
templates and are relatively easy for the programmer to use.
This chapter not only demonstrates the
basics of templates, it is also an introduction to containers, which are
fundamental components of object-oriented programming and are almost completely
realized through the containers in the Standard C++ Library. You’ll see
that this book has been using container examples – the Stash and
Stack – throughout, precisely to get you comfortable with
containers; in this chapter the concept of the iterator will also be
added. Although containers are ideal examples for use with templates, in Volume
2 (which has an advanced templates chapter) you’ll learn that there are
many other uses for templates as
well.
Containers
Suppose you want to create a
stack, as we have been doing throughout the book. This
stack class will hold ints, to keep it simple:
//: C16:IntStack.cpp
// Simple integer stack
//{L} fibonacci
#include "fibonacci.h"
#include "../require.h"
#include <iostream>
using namespace std;
class IntStack {
enum { ssize = 100 };
int stack[ssize];
int top;
public:
IntStack() : top(0) {}
void push(int i) {
require(top < ssize, "Too many push()es");
stack[top++] = i;
}
int pop() {
require(top > 0, "Too many pop()s");
return stack[--top];
}
};
int main() {
IntStack is;
// Add some Fibonacci numbers, for interest:
for(int i = 0; i < 20; i++)
is.push(fibonacci(i));
// Pop & print them:
for(int k = 0; k < 20; k++)
cout << is.pop() << endl;
} ///:~
The class IntStack is a trivial
example of a push-down stack. For simplicity it has been created here with a
fixed size, but you can also modify it to automatically expand by allocating
memory off the heap, as in the Stack class that has been examined
throughout the book.
main( ) adds some integers to
the stack, and pops them off again. To make the example more interesting, the
integers are created with the fibonacci( )
function, which generates the traditional rabbit-reproduction numbers. Here is
the header file that declares the function:
//: C16:fibonacci.h
// Fibonacci number generator
int fibonacci(int n); ///:~
Here’s the
implementation:
//: C16:fibonacci.cpp {O}
#include "../require.h"
int fibonacci(int n) {
const int sz = 100;
require(n < sz);
static int f[sz]; // Initialized to zero
f[0] = f[1] = 1;
// Scan for unfilled array elements:
int i;
for(i = 0; i < sz; i++)
if(f[i] == 0) break;
while(i <= n) {
f[i] = f[i-1] + f[i-2];
i++;
}
return f[n];
} ///:~
This is a fairly efficient
implementation, because it never generates the numbers more than once. It uses a
static array of
int, and relies on the fact that the compiler will initialize a
static array to zero. The first for loop moves the index i
to where the first array element is zero, then a while loop adds
Fibonacci numbers to the array until the desired element is reached. But notice
that if the Fibonacci numbers through element n are already initialized,
it skips the while loop
altogether.
The need for containers
Obviously, an integer stack isn’t a
crucial tool. The real need for containers comes when you start making objects
on the heap using new and destroying them with delete. In the
general programming problem, you don’t know how many objects you’re
going to need while you’re writing the program. For example, in an
air-traffic control system you don’t want to limit the number of planes
your system can handle. You don’t want the program to abort just because
you exceed some number. In a computer-aided design system, you’re dealing
with lots of shapes, but only the user determines (at runtime) exactly how many
shapes you’re going to need. Once you notice this tendency, you’ll
discover lots of examples in your own programming situations.
C programmers who rely on virtual memory
to handle their “memory management” often find the idea of
new,
delete, and container classes disturbing. Apparently, one practice in C
is to create a huge global array, larger than anything the program would appear
to need. This may not require much thought (or awareness of
malloc( ) and free( )), but it produces programs that
don’t port well and that hide subtle bugs.
In addition, if you create a huge global
array of objects in C++, the constructor and destructor overhead can slow things
down significantly. The C++ approach works much better: When you need an object,
create it with new, and put its pointer in a container. Later on, fish it
out and do something to it. This way, you create only the objects you absolutely
need. And usually you don’t have all the initialization conditions
available at the start-up of the program. new allows you to wait until
something happens in the environment before you can actually create the
object.
So in the most common situation,
you’ll make a container that holds pointers to some objects of interest.
You will create those objects using new and put the resulting pointer in
the container (potentially upcasting it in the process), pulling it out later
when you want to do something with the object. This technique produces the most
flexible, general sort of
program.
Overview of templates
Now a problem arises. You have an
IntStack, which holds integers. But you want a stack that holds shapes or
aircraft or plants or something else. Reinventing your source code every time
doesn’t seem like a very intelligent approach with a language that touts
reusability. There must be a better way.
There are three techniques for source
code reuse in this situation: the C way, presented here for contrast; the
Smalltalk approach, which significantly affected C++; and the C++ approach:
templates.
The C solution. Of course
you’re trying to get away from the C approach because it’s messy and
error prone and completely inelegant. In this approach, you copy the source code
for a Stack and make modifications by hand, introducing new errors in the
process. This is certainly not a very productive
technique.
The Smalltalk solution. Smalltalk
(and Java, following its example)
took a simple and straightforward approach: You want to
reuse code, so use inheritance.
To
implement this, each container class holds items of the generic base class
Object (similar to the example at the end of Chapter 15). But because the
library in Smalltalk is of such fundamental importance, you don’t ever
create a class from scratch. Instead, you must always inherit it from an
existing class. You find a class as close as possible to the one you want,
inherit from it, and make a few changes. Obviously, this is a benefit because it
minimizes your effort (and explains why you spend a lot of time learning the
class library before becoming an effective Smalltalk
programmer).
But it also means that all classes in
Smalltalk end up being part of a single inheritance tree. You must inherit from
a branch of this tree when creating a new class. Most of the tree is already
there (it’s the Smalltalk class library), and at the root of the tree is a
class called Object – the same class that each Smalltalk container
holds.
This is a neat trick because it means
that every class in the Smalltalk (and
Java[59])
class hierarchy is derived from Object, so every class can be held in
every container (including that container itself). This type of single-tree
hierarchy based on a fundamental generic type (often named Object, which
is also the case in Java) is referred to as an “object-based
hierarchy.” You may have heard this term and assumed it was some new
fundamental concept in OOP, like polymorphism. It simply refers to a class
hierarchy with Object (or some similar name) at its root and container
classes that hold Object.
Because the Smalltalk class library had a
much longer history and experience behind it than did C++, and because the
original C++ compilers had no container class libraries, it seemed like a
good idea to duplicate the Smalltalk library in C++. This was done as an
experiment with an early C++
implementation[60],
and because it represented a significant body of code, many people began using
it. In the process of trying to use the container classes, they discovered a
problem.
The problem was that in Smalltalk (and
most other OOP languages that I know of), all classes are automatically derived
from a single hierarchy, but this isn’t true in C++. You might have your
nice object-based hierarchy with its container classes, but then you might buy a
set of shape classes or aircraft classes from another vendor who didn’t
use that hierarchy. (For one thing, using that hierarchy imposes overhead, which
C programmers eschew.) How do you insert a separate class tree into the
container class in your object-based hierarchy? Here’s what the problem
looks like:
Because C++ supports multiple independent
hierarchies, Smalltalk’s object-based hierarchy does not work so
well.
The solution seemed obvious. If you can
have many inheritance hierarchies, then you should be able to inherit from more
than one class: Multiple
inheritance will solve the problem. So you do the following (a similar example
was given at the end of Chapter 15):
Now OShape has
Shape’s characteristics and behaviors, but because it is also
derived from Object it can be placed in Container. The extra
inheritance into OCircle, OSquare, etc. is necessary so that those
classes can be upcast into OShape and thus retain the correct behavior.
You can see that things are rapidly getting messy.
Compiler vendors invented and included
their own object-based container-class hierarchies, most of which have since
been replaced by template versions. You can argue that multiple inheritance is
needed for solving general programming problems, but you’ll see in Volume
2 of this book that its complexity is best avoided except in special
cases.
The template solution
Although an object-based hierarchy with
multiple inheritance is conceptually straightforward, it turns out to be painful
to use. In his original
book[61]
Stroustrup demonstrated what he considered a preferable alternative to the
object-based hierarchy. Container classes were created as large preprocessor
macros with arguments that could
be substituted with your desired type. When you wanted to create a container to
hold a particular type, you made a couple of macro calls.
Unfortunately, this approach was confused
by all the existing Smalltalk literature and programming experience, and it was
a bit unwieldy. Basically, nobody got it.
In the meantime, Stroustrup and the C++
team at Bell Labs had modified his original macro approach, simplifying it and
moving it from the domain of the preprocessor into the compiler. This new
code-substitution device is called a
template[62],
and it represents a completely different way to reuse code. Instead of reusing
object code, as with inheritance and composition, a template reuses source
code. The container no longer holds a generic base
class called Object, but instead it holds an unspecified parameter. When
you use a template, the parameter is substituted by the compiler, much
like the old macro approach, but cleaner and easier to use.
Now, instead of worrying about
inheritance or composition when you want to use a container class, you take the
template version of the container and stamp out a specific version for your
particular problem, like this:
The compiler does the work for you, and
you end up with exactly the container you need to do your job, rather than an
unwieldy inheritance hierarchy. In C++, the template implements the concept of a
parameterized type. Another benefit of the template approach is that the
novice programmer who may be unfamiliar or uncomfortable with inheritance can
still use canned container classes right away (as we’ve been doing with
vector throughout the
book).
 |
|