Am avut 371686 vizite de la lansarea siteului.




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).

Home   |   Web Faq   |   Radio Online   |   About   |   Products   |   Webmaster Login

The quality software developer.™
© 2003-2004 ruben|labs corp. All Rights Reserved.
Timp de generare a paginii: 17583 secunde
Versiune site: 1.8 SP3 (build 2305-rtm.88542-10.2004)