Tải bản đầy đủ - 0 (trang)
Chapter 17. Templates: Yet Another Design Tool

Chapter 17. Templates: Yet Another Design Tool

Tải bản đầy đủ - 0trang

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



cannot use this function to sort inventory items. Often, you cannot use it to sort even double

floating-point values. C++ templates allow the programmer to eliminate this limitation. With

templates, you can design generic classes and algorithms and then specify what type of component

should be handled by a specific container object or by a specific function call.

Programming with exceptions is used to streamline code that implements complex logic. Usually,

processing algorithms use C++ if or switch statements to separate normal processing of data from

processing of erroneous or faulty data. For multistep algorithms, the segments of source code for

the main algorithm and for the exceptional condition are written in alternative branches of the same

source code, and this often makes the source code harder to read¡Xthe main line is lost in the

multitude of exceptional and rare cases. C++ exceptions allow the programmer to isolate

exceptional cases in other, remote, segments of source code and streamline the base processing so

that it is easier to understand.

These language features, templates and exceptions, share several common characteristics: They are

complex, they increase the size of the executable code of the applications you write, and they

introduce additional execution time overhead.

Space and time overhead is the immediate result of the power and complexity of these

programming techniques. If you write real-time applications under severe memory and execution

speed constraints, you probably should not use templates and exceptions. If your applications are

going to be run on computers with plenty of memory and with fast processors, then space and time

constraints are not that important.

Still, it might be a good idea to introduce these language features into your programs gradually. If

these techniques streamline your source code only marginally, it might not be worth the trouble. As

is often the case in programming, the compromise between advantages and disadvantages is in the

eye of the beholder. Make sure that in your pursuit of interesting and challenging language features

you do not make the life of the maintainer too difficult.

In this chapter, I will discuss programming with C++ templates. In the next chapter, I will cover

C++ exceptions and other advanced language features that did not fit into the previous chapters.



A Simple Example of a Class Design Reuse

The strong typing approach of C++ allows the compiler to spot programming errors when the

programmer uses one type instead of another. C++ allows a number of exceptions to this rule.

Numeric values can be used interchangeably. Programmer-defined types can be used instead of

other types provided that conversion constructors and conversion operators are available. Classes

related through inheritance also allow limited substitution.

Still, many limitations on the use of typed values remain. Many algorithms are essentially the same

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (1050 of 1187) [8/17/2002 2:58:09 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



regardless of the type of values they operate on. For example, searching for a given account in the

array of account objects requires going through each component of the array and comparing the

owner name with the given name. Similarly, searching for a given inventory item in the array of

items requires going through each component of the array and comparing the item id with the given

id. These are the same actions, but you cannot pass an array of inventory items as a parameter to a

function that implements the search in the array of accounts. You have to write another function.

This function will be almost identical to the account search function. The only difference will be in

the comparison operation: One function compares the given name with the owner name in the

account object, and another function compares the given id with the id in the item object.

Container classes¡Xstacks, queues, lists, trees, and others¡Xcan contain different kinds of

components. Often, component classes handle their components in a similar way regardless of the

component type. For example, stack operations¡Xpushing the new component on the top of the

stack, popping a component from the top of the stack, and checking whether the stack is empty or

has any components left¡Xdo not depend on the nature of the component. They are done in the

same way whether the components are characters, accounts, or inventory items. It would be nice to

be able to design a generic stack and use it for any type of component that the application requires.

C++ strong typing makes this impossible. A stack of characters contains characters and cannot

contain account objects or inventory items. And a stack of accounts contains account objects and

cannot contain characters or inventory items.

Let us consider a simple example¡Xa stack of characters. It is a popular data structure. It is used in

compilers, calculators, screen managers, and in other applications where the collection of items

should support the LIFO (last in, first out) protocol. The example of checking parentheses in the

expression in Chapter 8, "Object-Oriented Programming with Functions," was using the stack (I

called it temporary storage) as the underlying data structure. The stack in my next example

allocates the required number of characters on the heap dynamically and supports the push(),

pop() and isEmpty() operations. The pop operation always retrieves the top symbol of the stack,

the one that was last pushed on the stack. The next symbol is always pushed on the top of the stack

so that it is the first one to be popped out.

class Stack {

char *items;

int top, size;

public:

Stack(int);

void push(char);

char pop();

bool isEmpty() const;

~Stack();

} ;



// stack of character symbols

// current top, total size

//

//

//

//

//



conversion constructor

push on top of stack

pop the top symbol

is stack empty?

return heap memory



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (1051 of 1187) [8/17/2002 2:58:09 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



Listing 17.1 shows the implementation of the stack along with the test driver for the class. The

conversion constructor uses the initialization list to initialize class data members: the total size of

the character array requested for the stack and the current position of the stack top in the array (the

index of the location where the next symbol will be inserted). The constructor allocates the heap

memory using the size requested by the client code. If the system is out of memory, the execution is

terminated.

The function push() inserts its parameter value into the heap array. Since the size of the heap array

is requested by the client code, and the client code should know how much stack storage it needs

for its algorithm, it is all right to terminate execution in case of array overflow. However, this

would pop too much responsibility up to the client code. Meanwhile, the client code should

concentrate on its algorithm (e.g., checking whether parentheses match) and not with the user

interface for diagnostic messages. It would be more appropriate to push the responsibility for

dealing with overflow down to the server class.

One alternative for handling array overflow is to terminate program execution. The advantage of

doing this in the server class rather than in the client code is that the client code is streamlined and

does not contain error processing related to the implementation details of the server. Another, better

alternative is to process the server problem (overflow) in the server and not in the client code. This

can be done, for example, by allocating additional memory in the server object in case of array

overflow.

How much additional memory to allocate is debatable. In Listing 17.1, I allocate a stack array of

double the current size, copy existing stack contents into the newly allocated array, dispose of the

existing array, and continue operations using the heap array that is twice as long as its previous

version. The client code is totally insulated from these details of memory management.

Function pop() is straightforward¡Xit just pops the top character from the stack and updates the

index that points to the top of the stack. For a large data structure, it would be appropriate to watch

the position of the top and return the existing memory when, for example, half of the existing array

is not used. For this simple example, there is no need to do that.

Function pop() could check whether the stack is empty and send a message (or a return value) if

there is nothing to pop from the stack. I felt that this approach, although possible, would make

communications between the stack class and its clients unnecessarily complex. Also, what would a

client do if it tries to pop the stack when the stack is empty? In most cases (see, e.g., Chapter 8 and

its examples in Listing 8.10-8.13), the empty stack is a signal to the client to stop one phase of

processing and to start another phase. Hence, there is no need to involve the server class into this

application-related decision. The client code should call the stack method isEmpty() before each

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (1052 of 1187) [8/17/2002 2:58:09 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



call to pop() and either call the pop() method if the stack is not empty or do something else. In

Listing 17.1, I terminate the algorithm¡Xthe empty stack signals the end of processing.

The last two methods, the method isEmpty() and the Stack destructor, are trivial. The isEmpty()

method checks whether the stack index has returned into its initial position. The destructor returns

the heap memory allocated to the object during its lifetime.

Class Stack objects can only be used to store the elements of given type, not for other operations.

These objects are not meant to initialize one another or to be assigned to one another. Formally,

you can perform these operations on any C++ variables, including Stack objects. Actually, if

somebody uses a Stack object in initialization or in assignment, this should not be supported. This

means that adding the copy constructor and the assignment operator to class Stack is overkill. On

the other hand, making their prototypes private is helpful. For example, if one wants to pass a

Stack object by value, this will be a syntax error.

For illustration purposes, Listing 17.1 initially allocates a very small array for the Stack object.

This is why you can see debugging messages that report the change in the array size. The output of

the program is shown in Figure 17-1.



Figure 17.1. Output for program in Listing 17.1.



Example 17.1. Class Stackthat contains characters.

#include

using namespace std;

class Stack {

char *items;

int top, size;

Stack(const Stack&);

operator = (const Stack&);

public:

Stack(int);

void push(char);

char pop();

bool isEmpty() const;

~Stack();

} ;



// stack of character symbols

// current top, total size



//

//

//

//

//



conversion constructor

push on top of stack

pop the top symbol

is stack empty?

return heap memory



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (1053 of 1187) [8/17/2002 2:58:09 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



Stack::Stack(int sz = 100) : size(sz),top(0)

{ items = new char[sz];

if (items==0)

{ cout << "Out of memory\n";

exit(1); } }

void Stack::push (char c)

{ if (top < size)

items[top++] = c;

else // recover from stack overflow

{ char *p = new char[size*2];

if (p == 0)

{ cout << "Out of memory\n"; exit(1); }

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

p[i] = items[i];

delete [] items;

items = p;

size *= 2;

cout << "New size: " << size << endl;

items[top++] = c; } }

char Stack::pop()

{ return items[-top]; }

bool Stack::isEmpty() const

{ return top == 0; }

Stack::~Stack()

{ delete [] items; }

int main()

{

char data[] = "abcdefghij";

Stack s(4);

int n = sizeof(data)/sizeof(char)-1;

cout << "Initial data: ";

for (int j = 0; j < n; j++)

{ cout << data[j] << " "; }

cout << endl;

for (int i = 0; i < n; i++)

s.push(data[i]);

cout << "Inversed data: ";

while (!s.isEmpty())

cout << s.pop() << " ";

cout << endl;

return 0;

}



// allocate heap memory



// pass by reference

// normal case: push symbol



// get more heap memory

// test for success

// copy existing stack

// return heap memory

// hook up new memory

// update stack size

// push symbol on top



// pop unconditionally

// anything to pop?



// return heap memory



// pre-canned input data

// Stack object

// input data count

// print initial data



// push data on the stack



// pop until stack is empty



The problem with this design is that if you want to have a container for other types of components,

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (1054 of 1187) [8/17/2002 2:58:09 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



the design has to be repeated from scratch. All instances of the previous component type should be

replaced by instances of another component type. For example, if you want to have a stack of

integers instead of characters, the stack specification should look this way.

class Stack {

int *items;

int top, size;

Stack(const Stack&);

operator = (const Stack&);

public:

Stack(int);

void push(int);

int pop();

bool isEmpty() const;

~Stack();

} ;



// stack of integer symbols

// current top, total size



//

//

//

//

//



conversion constructor

push on top of stack

pop the top symbol

is stack empty?

return heap memory



For a stack of double floating-point values, the class has to be modified again.

class Stack {

double *items;

int top, size;

Stack(const Stack&);

operator = (const Stack&);

public:

Stack(int);

void push(double);

double pop();

bool isEmpty() const;

~Stack();

} ;



// stack of double symbols

// current top, total size



//

//

//

//

//



conversion constructor

push on top of stack

pop the top symbol

is stack empty?

return heap memory



Notice that a global editor is not sufficient to do the job. To arrive at this design, I had to change

type int to double in the pointer definition, in the push() parameter list, and in the pop() return

value. The definition of data members top and size must not be changed; the type of the

constructor parameter does not change either. Hence, the reuse of this design requires attention.

This is by no means a no-brainer.

Another method of reusing the container is to design it with a generic "parameter" type. This type

corresponds neither to a programmer-defined type nor to a built-in type. For example, class Stack

can be defined in the following way.



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (1055 of 1187) [8/17/2002 2:58:09 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



class Stack {

Type *items;

int top, size;

Stack(const Stack&);

operator = (const Stack&);

public:

Stack(int);

void push(Type);

Type pop();

bool isEmpty() const;

~Stack();

} ;



// stack of symbols of type Type

// current top, total size



//

//

//

//

//



conversion constructor

push on top of stack

pop the top symbol

is stack empty?

return heap memory



This code does not compile unless the compiler knows what type Type is. Once you have defined

it, the code compiles. This is nice because it makes the job of substitution simpler and less error

prone. You should replace the instances of use of the type Type but not others. Moreover, the type

Type can be defined using the typedef definition, for example:

typedef char Type;



// type is equivalent to char



This definition has to be seen by the compiler before it processes the Stack definition. The

compiler will replace each instance of the identifier Type with the keyword char and will compile

the resulting class.

With this approach, the reuse of the class design is no longer impaired by random errors. All that is

needed to generate a version of the stack for another type of component is to replace the keyword

char in the typedef statement with the name of another type. There is no danger of accidental

errors. Listing 17.2 shows the version of the class Stack where the type Type denotes type int.

The output of this program is shown in Figure 17-2.



Figure 17.2. Output for program in Listing 17.2.



Example 17.2. Reuse of class design for a Stack that contains integers.

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (1056 of 1187) [8/17/2002 2:58:09 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



#include

using namespace std;

typedef int Type;



// portable type definition



class Stack {

Type *items;

int top, size;

Stack(const Stack&);

operator = (const Stack&);

public:

Stack(int);

void push(const Type&);

Type pop();

bool isEmpty() const;

~Stack();

} ;



// stack of items of type Type

// current top, total size



//

//

//

//

//



Stack::Stack(int sz = 100) : size(sz),top(0)

{ items = new Type[sz];

if (items==0)

{ cout << "Out of memory\n"; exit(1); } }



// allocate heap memory



void Stack::push (const Type& c)

{ if (top < size)

items[top++] = c;

else // recover from stack overflow

{ Type *p = new Type[size*2];

if (p == 0)

{ cout << "Out of memory\n"; exit(1); }

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

p[i] = items[i];

delete [] items;

items = p;

size *= 2;

cout << "New size: " << size << endl;

items[top++] = c; } }

Type Stack::pop()

{ return items[-top]; }

bool Stack::isEmpty() const

{ return top == 0; }

Stack::~Stack()

{ delete [] items; }



conversion constructor

push on top of stack

pop the top symbol

is stack empty?

return heap memory



// pass by reference

// normal case: push symbol



// get more heap memory

// test for success

// copy existing stack

// return heap memory

// hook up new memory

// update stack size

// push symbol on top



// pop unconditionally

// anything to pop?



// return heap memory



int main()

{

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (1057 of 1187) [8/17/2002 2:58:09 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



Type data[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 } ;

Stack s(4);

// stack object

int n = sizeof(data)/sizeof(Type);

// input data count

cout << "Initial data: ";

for (int j = 0; j < n; j++)

{ cout << data[j] << " "; }

// print input data

cout << endl;

for (int i = 0; i < n; i++)

{ s.push(data[i]); }

// push data on the stack

cout << "Inversed data: ";

while (!s.isEmpty())

// pop until stack is empty

cout << s.pop() << " ";

cout << endl;

return 0;

}



The typedef approach allows you to reuse the class design, not only for built-in types but for

arbitrary programmer-defined types as well¡Xaccounts, inventory items, rectangles, and so on. The

caveat here is the ability of the component type to support operations that the component objects

undergo within the container class. This is not too difficult, but you should make sure you

recognize these operations in the container code¡Xthey are often implicit.

In the Stack example, the container class creates an array of components on the heap. This means

that the component class has to provide a default constructor. This is not a problem for built-in

types, but might be a problem for a programmer-defined type.

When a component object is inserted into the container in the push() method, the assignment is

used. If the component class does not handle its memory dynamically, this is not a problem. If it

does handle heap memory, the component class has to provide an overloaded assignment operator.

Notice that the assignment operator for the container remains private¡XI am talking about the

assignment operator for the component class.

Another reuse issue that requires support from the design of the component class is parameter

passing and returning values from container methods. For built-in data types, this issue is trivial.

This is why in Listing 17.1, method push() had a value parameter, and method pop() was

returning the value. In Listing 17.2, method push() passes the parameter as a constant reference to

avoid integrity problem and negative impact on performance (see Chapter 11, "Constructors and

Destructors: Potential Trouble," for a detailed discussion of these problems). Method pop() still

returns the value for compatibility with the first version of the program in Listing 17.1. However,

many container designers avoid returning values from container methods and pass reference

parameters (non-constant) instead.

This approach allows us to reuse the container design in another program for any type that supports

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (1058 of 1187) [8/17/2002 2:58:09 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



assignment and copying. However, if stacks of different types are used in the same program, this

approach does not work. The type Type can have only one meaning during compilation.

When the same container is to be used for different types of components in the same program, we

are back to the technique of manual editing of design. In the case of the stack, each stack should

have a different name, for example, charStack, intStack, pointStack, and so on, and their

code and interfaces should be edited.

class doubleStack {

double *items;

int top, size;

Stack(const Stack&);

operator = (const Stack&);

public:

Stack(int);

void push(double);

double pop();

bool isEmpty() const;

~Stack();

} ;



// edit component type

// leave the type the same



// leave the type the same

// edit parameter type

// edit return type



If source code for each class is edited individually, propagation of future modifications becomes

cumbersome and error prone. Unique class names clog the project name space and create the

potential for name conflicts.

Use of the macro facility can automate the generation of new class names and code, but this method

of reuse is cumbersome and error prone. I do not think that C++ programmers today should learn

how to write macros¡Xthis is an obsolete approach to design reuse. Just to satisfy your curiosity,

this is how the macro for this stack looks.

#define MakeName(a,b) a/**/b

#define DefineStack(Type)

\

class MakeName(Type,Stack) {

\

Type *items;

\

int top, size;

\

Stack(const Stack&);

\

operator = (const Stack&);

\

public:

\

Stack(int sz = 100) : size(sz),top(0) \

{ items = new Type[sz];

\

if (items==0)

\

{ cout << "Out of memory\n"; exit(1); } }



\



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (1059 of 1187) [8/17/2002 2:58:10 PM]



file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm



void push(const Type& c)

\

{ if (top < size)

\

items[top++] = c;

\

else

\

{ Type *p = new Type[size*2]; \

if (p == 0)

\

{ cout << "Out of memory\n"; exit(1); } \

for (int i=0; i
p[i] = items[i];

\

delete [] items;

\

items = p;

\

size *= 2;

\

cout << "New size: " << size << endl; \

items[top++] = c; } }

\

Type pop()

\

{ return items[¡Xtop]; }

\

bool isEmpty() const

\

{ return top == 0; }

\

~Stack()

\

{ delete [] items; }

\

} ;



The client must first define stack types using the DefineStack name defined at the start of the

macro.

DefineStack(int);



This will generate the name intStack as a concatenation of the type (specified in parentheses) and

the name Stack (the second argument to the MakeName macro). This will also define code for the

stack of integers. Then the client will be able to declare and use an appropriate stack object.

intStack s(4);



Since all code fits into one preprocessor-generated line, it is difficult to debug. Lexical substitution,

as is often the case with macros, can generate incorrect code. This is not a good way to reuse class

design.



Syntax of Template Class Definition

C++ supports yet another method to reuse class design. This tool is called a template class. Instead

of the class with a fixed type of component, you create the class where the type of component is

file://///Administrator/General%20English%20Learning/it2002-7-6/core.htm (1060 of 1187) [8/17/2002 2:58:10 PM]



Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Chapter 17. Templates: Yet Another Design Tool

Tải bản đầy đủ ngay(0 tr)

×