Here, each instantiation of class Stack e.g., for type float has access to details of each instantiation of class Node e.g., of class Point.. Node *next; // Node *next' is also OK Nodec
Trang 1to be consistent across different functions of the template class Here is another stack function where I use different names for the type parameter and for the expression parameter.
{ cout << "Out of memory\n"; exit(1); }
for (int i=0; i < size; i++) // copy existing stack
p[i] = items[i];
delete [] items; // return heap memory
items = p;
size *= 2; // update stack size
cout << "New size: " << size << endl;
items[top++] = c; } } // push symbol on top
Similar to type parameters, expression parameters have to be listed both in the template parameter list and in the class name prefix for all member functions Even if an expression parameter is not used within the body of the function, it still has to be listed
template <class Type, int sz>
Type Stack<Type,sz>::pop() // parameters are not used
{ return items[¡Xtop]; }
template <class Tp, int s>
bool Stack<Tp,s>::isEmpty() const // parameters are not used
Stack<int,4> s; // stack object
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 2Stack<int,8> s1; // incompatible stack object
Consider, for example, a global function DebugPrint(). It has the parameter of class
Stack<int,4>. Notice that the parameter object is passed by reference¡Xthe private declaration of the Stack<Type,sz> copy constructor prevents passing stack objects by value Also notice that the parameter is not labeled as const because it changes (even temporarily) during function execution
void DebugPrint(Stack<int,4>& s) // no const modifier
{ Stack<int,4> temp;
cout << "Debugging print: ";
while (!s.isEmpty()) // pop until stack is empty
{ int x = s.pop(); temp.push(x); // save in temporary stack
cout << x << " "; } // print each component
cout << endl;
while (!temp.isEmpty()) // pop until stack is empty
{ s.push(temp.pop()); } } // restore initial state
The stack objects s and s1, are of different types Object s can be passed as a parameter to
DebugPrint(). An attempt to do so with object s1 causes a syntax error
const int length = 4;
Stack<int,length> s2; // compatible with Stack<int,4>
As far as templates with type parameters only are concerned, all instantiations with the same actual type arguments are of the same type, and one object can be used instead of another object Here we reconsider the template class with one type parameter
Trang 3Type *items; // stack of items of type Type
int top, size; // current top, total size
Stack(const Stack& = 100);
operator = (const Stack&);
public:
Stack(int); // conversion constructor
void push(const Type&); // push on top of stack
Type pop(); // pop the top symbol
bool isEmpty() const; // is stack empty?
~Stack(); // return heap memory
Relations Between Instantiations of Template Classes
Template instantiations can be used as actual type arguments to instantiate other template classes For example, you can create a stack of dictionary entries with the following declaration
Stack<DictEntry<Point,char*> > stackOfEntries; // 100 entries
Notice an extra space between the two greater than signs If you do not insert this space, the
compiler will misunderstand the code and will shower you with a deluge of irrelevant error
messages None of these messages indicates that you need an extra space This is the second place where C++ is not space blind Another place where space is important is defining the default
parameter value for a function parameter of a pointer type where the name of the parameter is not used
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 4In the declaration above, the Stack instantiation prompts the DictEntry instantiation An
optimized compiler might cache object code for reuse in future compilations If the compiler does not do that, compilation and link time can grow significantly
Template instantiations for different actual types (and expression values) are separate and have no relation or access to each other
For example, DictEntry<int,int> and DictEntry<float,record> are two independent distinct classes So are instantiations for Stack<int> and Stack<float>. Objects of these types cannot be used one instead of another
A class can declare all its template instantiations to have a common base non-template class:
Template Classes as Friends
A non-template class (or function) can be declared as a friend of all instantiations of a template class, if the use of instantiated objects does not depend on their type:
Trang 5Node *next; // Node *next' is also OK
Node(const int val) : item(val)
{ next = NULL; }
} ;
Here the Node class can support the information field and the link to the next node in the linked list
It does not need any member functions with the exception of the constructor that initializes both data fields
Each instantiation of the Stack class is a friend of the non-template class Node and has access to its non-public members This might be useful if factoring out common code decreases the size (and compile/link time) of the object code Instead of heap memory allocated at instantiation (or at array overflow), the Stack class can allocate a Node object every time data is pushed on the stack and deallocate the top Node object when the data is popped from the stack
However, this is not very useful One type Node (e.g., with the integer information field) cannot accommodate different types of objects that the client code wants to push on the stack This means that class Node has to be a template as well
This also means that class Node should be defined as a template class Then different types of
Stack objects can instantiate and access different types of the Node object with different types of the item field
Node<Type> *next; // Node *next' is also OK
Node(const Type& val) : item(val)
{ next = NULL; }
} ;
The technical term for this use of templates is unbounded types The parameter Type is independent
of parameter T, and each parameter can accept any actual values independently of each other
Come to think of it, this is more than you need Here, each instantiation of class Stack (e.g., for type float) has access to details of each instantiation of class Node (e.g., of class Point) This code does not implement a realistic model of the real world
We need to enforce a one-to-one mapping between related instantiations, so that a stack of integers
becomes a friend of an integer node only, not of a node with other types of the item field To
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 6achieve that mapping, we can bind a friend (client) template class (in this case, Stack) to the same type(s) as the template class that provides services (in this case, Node).
Node<Type> *next; // Node *next' is also OK
Node(const Type& val) : item(val)
{ next = NULL; }
} ;
Here, for each instantiation of Node to a specific type (e.g., of class Point,) the Stack instantiation
to the same type (class Point) is made a friend to this instantiation of class Node.
Class Stack now has a data member of class Node pointer that is initialized to zero in the Stack
constructor When the next node is pushed on the stack, this pointer points to the new node (and the new node points to the node that used to be the first node in the list) The member function
isEmpty() checks whether this pointer is NULL or points to a node This means that function pop()
has to set this pointer to NULL when the last node is removed from the stack
int isEmpty() const
{ return top == NULL; } // does top point to a node?
~Stack();
} ;
As for any template, the use of Node outside of Node definition must be qualified with the
parameter list This is why the Stack data member top cannot be of type Node*¡Xit should be of type Node<T>*, where T is the Stack type parameter As a result of this qualification in the Stack
definition, instantiation of a Stack class object results in the automatic instantiation of a Node class data field of the same type
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 7The Stack method push() allocates a new Node object on the heap The call to the Node
constructor initializes the item field of the Node object to the value of the push() parameter The
next field of the new Node is set to point to the node that the Stack field top is currently pointing
to, and the top field is reset to point to the new Node object
template <class T>
void Stack<T>::push (const T& val)
{ Node<T> *p = new Node<T>(val); // type Node<T>, not Node
if (p == NULL)
{ cout << "Out of memory\n"; exit(1); }
p->next = top; // point it to first node
top = p; } // point to new node
There is no need to test for array overflow in push() because there is no array in this
implementation There is still the need to test whether the allocation of the Node object is
successful Notice the type of the pointer¡Xit is not Node*; it is Node<T>*. Similarly, when the heap space is requested by the operator new, it is of type Node<T>, not of type Node, The type T
will be provided at the time of Stack instantiation
The Stack method pop() sets the local pointer (of type Node<T>, not just Node) to point to the first node of the stack, copies the information field into a local variable (of type T), moves the top field
to point to the second node, and deletes the top node because it is not needed anymore
template <class T>
T Stack<T>::pop() // return value of type T
{ Node<T> *p = top; // Node of type T, not Node
T val = top->item; // get the value of type T
top = top->next; // point to the second node
delete p; // return top node to heap
Trang 8The Stack destructor has to scan the linked list of remaining nodes and return them to the heap The local pointer p of type Node<T> with the component of type T is used again It is set to point to the first node of the list Notice that the pointer to the first node of the list, the data member top, is
of the same type as pointer p: Node<T>. In a while loop, the top pointer moves to the next node, the node pointed to by the pointer p is deleted, and pointer p moves to point to the next node as well
template <class T>
Stack<T>::~Stack()
{ Node<T> *p = top; // type Node of type T
while (top != NULL) // top is 0 when no nodes
{ top = top->next; // point to the next node
delete p; // delete the previous node
p = top; } } // traverse to the next node
Another approach is to provide each client with its own private server class If the Node definition
is nested within the private section of the client, then only that client (and its friends) can access class Node. This again raises the issue of coordination (mapping) between template definitions
Nested Template Classes
With nested design, the definition of the template class Node is nested within the definition of the container class that handles Node objects Since the Node definition is entirely within the scope of the container class (e.g., Stack,) the name Node is not visible to other potential clients (e.g., Queue
and List.) Hence, Node members can be made public, and there is no need to declare its single client (e.g., Stack) as a friend to class Node.
This is the first attempt on the design with nested classes Class Node is defined using the keyword
struct and all its members are public
template <class T>
class Stack {
template <class Type> // Is it legal? Is it needed?
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 9struct Node {
Type item;
Node<Type> *next; // type depends on instantiation
Node(const Type& val) : item(val)
int isEmpty() const
{ return top == NULL; } // does top point to a node?
Node should be one-to-one rather than one-to-many: Class Stack needs a Node object that is
instantiated to the same actual type as class Stack itself
A good way to achieve that is to define class Stack as a template and then define class Node as a regular non-template class that uses the Stack type parameter for its data member and for its method parameter types
template <class T>
class Stack {
struct Node { // it depends on parameter type
T item; // same type as in Stack
Node *next; // Node<T> is incorrect here Node(const T& val) : item(val) // same type as in Stack
int isEmpty() const
{ return top == NULL; } // does top point to a node? ~Stack();
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 10} ;
Each instantiation of the Stack templates generates a Node class that uses the same type as the
Stack actual type argument There is no need to qualify the type Node within the Stack definition
The same is true of Stack member functions When a local pointer is defined in a member
function, it is defined as a pointer to type Node, not a pointer to the type Node<T>. For example, method push() is almost the same as in the previous version, but the pointer p is defined
differently I commented out the previous version of the pointer definition so that you can compare both versions
template <class T>
void Stack<T>::push (const T& val)
// { Node<T> *p = new Node<T>(val); // type Node<T>, not Node
{ Node *p = new Node(val); // type Node, not Node<T>
if (p == NULL)
{ cout << "Out of memory\n"; exit(1); }
p->next = top; // point it to first node
top = p; } // point to new node
The same is true of other methods of the container class Listing 17.5 shows the implementation of the template class Stack with a nested class Node. The output of the program is shown in Figure 17-5
Figure 17.5 Output for program in Listing 17.5
Example 17.5 Example of a template class with a nested server class.
#include <iostream>
using namespace std;
template <class T>
class Stack {
struct Node { // it depends on parameter type
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 11T item; // same type as in Stack Node *next; // Node<T> is incorrect here Node(const T& val) : item(val) // same type as in Stack { next = NULL; } } ;
Node *top; // it is not a template now public:
Stack() // default: no initial length
{ top = NULL; }
void push(const T&);
T pop();
int isEmpty() const
{ return top == NULL; } // does top point to a node? ~Stack();
} ;
template <class T>
void Stack<T>::push (const T& val)
// { Node<T> *p = new Node<T>(val); // type Node<T>, not Node { Node *p = new Node(val); // type Node, not Node<T>
if (p == NULL)
{ cout << "Out of memory\n"; exit(1); }
p->next = top; // point it to first node top = p; } // point it to new node
template <class T>
T Stack<T>::pop() // return value of type T // { Node<T> *p = top; // type Node<T>, not Node { Node *p = top; // type Node, not Node<T>
T val = top->item; // get the value of type T top = top->next; // point to the second node delete p; // return top node to heap return val; }
template <class T>
Stack<T>::~Stack()
// { Node<T> *p = top; // type Node of type T
{ Node *p = top; // type Node of type T
while (top != NULL) // top is 0 when no nodes
{ top = top->next; // point to the next node
delete p; // delete the previous node
p = top; } } // traverse to the next node
int main()
{
int data[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 } ;
Stack<int> s; // stack object
int n = sizeof(data)/sizeof(int); // number of components
cout << "Initial data: ";
for (int j = 0; j < n; j++)
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 12{ 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
Templates with Static Members
If a template class declares static data, each template instantiation will have a separate set of these static members All objects that belong to this particular instantiation will share the same static member(s), but they will have no access to static members that belong to an instantiation for a different actual type parameter
For example, class Stack can declare its top data member static This is an interesting design
alternative¡Xwith the top data member static, the data fields item and next can be moved to the class Stack itself as a non-static data member What is left in class Node then? Nothing It becomes redundant Hence, this design allows one to get rid of class Node.
In this design, class Stack combines the roles of the Stack in previous examples (calls to push(), pop(), and isEmpty() member functions) and the role of the Node class (fields item and next.) This is why it has two constructors: the default constructor and the conversion constructor
The default constructor is called when a Stack object is instantiated in the client code It does not have to do anything, but it has to be there to eliminate a syntax error The conversion constructor is called from method push(): When a new node has to be allocated, push() creates a new Stack
object rather than a new Node object The constructor initializes the item field (to the value to be stored) and the next field (to point to the top stack node)
The pop() function deletes the top node using a local pointer The type of pointer is a pointer to
Stack<T>. Since it is a pointer to an object of type Stack, the Stack destructor (instead of the
Node destructor as in previous versions) is called In the previous versions, the Stack destructor
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 13deleted remaining stack nodes Here, this is harmful This is the reason that class Stack does not have a destructor (It has the default destructor that does nothing.)
template <class T> class Stack {
static Stack *top; // static data member
T item; // from Node
Stack *next; // from Node
public:
Stack() { } // create object in client
Stack(const T& val)
: item(val), next(top) // create new node in push()
{ top = this; }
void push(const T& val)
{ Stack<T> *p=new Stack<T>(val); } // no Node<T>, no Node
T pop()
{ Stack<T> *p = top;
T val = top->item; // no Node<T>, no Node
top = top->next; // point to second node
delete p; // delete top node: destructor
return val; }
int isEmpty() const
{ return top == NULL; }
void remove() // no call to destructor
{ Stack<T> *p = top; // trailing pointer
while (top != NULL)
{ top = top->next; // go to next node
delete p; // delete previous node
p = top; } } // catch up with next node
} ;
The absence of the destructor creates the danger of a memory leak To avoid it, class Stack
provides the remove() method that does the same thing as the Stack destructor in the previous versions The drawback of this design is that the client code has to explicitly call the remove()
method to dispose of remaining nodes on the stack (if any)
Initialization of the static member of the template class takes place not at the beginning of the program execution (as for static members of regular classes) but when a template object is
instantiated, because it is at this moment that the static member for this particular actual type is created
The syntax of the initialization statement (in a header file) should:
ϒΠ indicate that the static member belongs to template
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 14ϒΠ specify the type of the static member
ϒΠ specify the scope of the static member
ϒΠ specify its name and the initial value
This is how the initialization statement for the stack static data member top looks: Its type is
Stack<T>*, its scope is Stack<T>, its name is top, and its initial value is NULL.
template <class T> // it belongs to template
Stack<T>* Stack<T>::top = NULL;
Consider, for example, a template class Array that contains a set of data (of a component type) and allows the client code to check whether a given element (of the component type) can be found in the collection
template <class T>
class Array {
T *data; // heap array of data
int size; // size of the array
Trang 15Array(T items[], int n) : size(n) // conversion constructor
{ data = new T[n]; // allocate heap memory
if (data==0)
{ cout << "Out of memory\n"; exit(1); }
for (int i=0; i < n; i++) // copy input data
data[i] = items[i]; }
int find (const T& val) const
{ for (int i = 0; i < size; i++)
if (val == data[i]) return i; // return index if found
return -1; } // otherwise return -1
~Array()
{ delete [] data; }
} ;
This template class contains only a constructor, a method find(), and a destructor The
constructor allocates heap space sufficient for input data and copies the input array into the heap memory The method find() searches the heap array; if the parameter value is not found, it returns 1; if the value is found, the method returns the index of the value The destructor returns the heap memory
The client code instantiates the Array object of type int, initializes it, and prints the results of the search for the given value
int main()
{ int data1[] = { 1, 2, 3, 4, 5 } ;
int n1 = sizeof(data1)/sizeof(int); // number of components
cout << "Initial data: ";
for (int j = 0; j < n1; j++)
{ cout << data1[j] << " "; } // print input data
cout << endl;
Array<int> a1(data1,n1); // array object
int item1 = 3; int idx;
Array object is instantiated for the component of the character array type, both the constructor and the find() method have a problem
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 16container will point to the same character array in the client space Similarly, the find() method will compare the string addresses rather than the string contents You see that for a character array
as an Array component, the general form of the template class does not work¡Xit needs copying strings in the constructor and comparing strings in find().
C++ supports the concept of specialization to deal with the type arguments that need special
treatment For each special class, a separate specialized class template should be provided The syntax for describing the specialization is a mixture of the syntax for the template class itself (with the template parameter list) and the template initialization in the client code (with the actual type list) You take the type parameter from the template parameter list and move it into the actual type list If the brackets of the template parameter list become empty as a result, this is fine For
example, the Array template class header:
template <class T> // remove class T from brackets
class Array { // append <char*> to class name
becomes
template <> // empty template parameter list
class Array<char*> { // actual type list
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 17
Array<char*> a1(data2,n2); // specialized template object
Listing 17.6 shows the complete program that contains the template Array and its specialized
template for the components of the character array type The test driver initializes the template class object a1 and the specialized template object a2 and sends messages to each object The output of the program is shown in Figure 17-6
Figure 17.6 Output for program in Listing 17.6
Example 17.6 Example of a template class specialization.
#include <iostream>
using namespace std;
template <class T>
class Array {
T *data; // heap array of data
int size; // size of the array
{ cout << "Out of memory\n"; exit(1); }
for (int i=0; i < n; i++)
data[i] = items[i]; }
int find (const T& val) const
{ for (int i = 0; i < size; i++)
Trang 18} ;
template <> // empty template list
class Array <char *> { // type of specialization char* *data; // heap array of data
int size; // size of the array
Array(const Array&);
operator = (const Array&);
public:
Array(char* items[], int n) : size(n) // conversion
{ data = new char*[n]; // allocate heap memory
if (data==0)
{ cout << "Out of memory\n"; exit(1); }
for (int i=0; i < n; i++)
{ int len = strlen(items[i]); // special for strings only data[i] = new char[len+1];
strcpy(data[i],items[i]); } }
int find (const char*& val) const
{ for (int i = 0; i < size; i++)
if (strcmp(val,data[i])==0) // special for strings only return i;
char* data2[] = { "one", "two", "three", "four", "five" } ;
int n1 = sizeof(data1)/sizeof(int); // number of components int n2 = sizeof(data2)/sizeof(char*);
cout << "Initial data: ";
for (int j = 0; j < n1; j++)
{ cout << data1[j] << " "; } // print input data
cout << endl;
for (int i = 0; i < n2; i++)
{ cout << data2[i] << " "; } // print input data
cout << endl;
Array<int> a1(data1,n1); // array object
Array<char*> a2(data2,n2); // specialized object
int item1 = 3; int idx;
char* item2 = "three";
Trang 19}
C++ also supports partial specializations They provide specific treatment for only one of several type parameters For example, the template class DictEntry from Listing 17.4 supports two type parameters:
For the Key type instantiated to character array, a specialized template is created by moving the Key
parameter from the template parameter list and appending the specialized type (in angle brackets)
to the class name
template <class Data> // remove the Key type
class DictEntry <char*>{ // append specialized type
char* key; // replace the Key type
template < > // remove both parameter types
class DictEntry <char*, char*>{ // append specialized types
char* key; // replace the Key type
char* info; // replace the Data type
public:
¡K} ; // the rest of the class
If only the second parameter should be treated in a special way, this is not a problem, but you have
to repeat the first type parameter in the list of actual types
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 20
template <class Key>
class DictEntry <Key, char*> {
general template class to generate the object
Using specialization is often a necessity (when one component type requires special treatment) Using specializations makes programs larger, more complex, and more difficult to understand Not all compilers support specializations well When one of the data types requires special treatment (most often, it is a character array), consider writing a separate class with a separate name, for
example, CharArray. The advantage of writing a separate class is that there is no doubt which class is used to instantiate the object The disadvantage is that there is no guarantee that similar features are treated similarly in different special classes
Sometimes, this tradeoff is difficult to make C++ specialized classes give you one way to resolve the problem Decide for yourself what you want to use
Template Functions
A stand-alone nonmember function can be defined as a template; the syntax is similar to the syntax
of template class member functions
When the function needs a prototype, it also contains the template parameter list with each class
keyword followed by a parameter
Trang 21Both the definition and the prototype (forward declaration) start with the template keyword
followed by the formal parameter list in angle brackets; each formal parameter consists of the
keyword class followed by a programmer-defined identifier The keyword class and the
identifier are separated by commas The identifier must occur only once in the parameter list
template <class T, class T> // this is illegal
void swap(T& x, T& y)
{ T a = x; x = y; y = a; }
Each type parameter must be used in the parameter list of the template function If the type
parameter is not present in the parameter list, the compiler flags it as a syntax error
template <class T>
int isEmpty(void); // compile-time error for global function
Similar to non-template functions, template functions can be declared extern, inline, or
static; the specifier (if any) follows the template list of formal parameters and precedes the
function return type
int a=5, b=10; double c=3.0, d=4.0;
swap(a,b); // instantiation for integers
swap(c,d); // instantiation for double
Since compiler knows the types of the actual arguments a and b for the first call and c and d for the
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 22second call, it generates code for swap(int&,int&) and swap(double&,double&).
A return value is not considered for parameter matching; conversions can be done as needed
However, implicit conversions are not used for template arguments If the compiler cannot decide which function to generate to match arguments exactly, it is a syntax error
template < >
inline void swap <char*> (char* x, char* y)
{ char* a = new char[strlen(x)+1];
char* b = new char[strlen(y)+1];
if (b==NULL) { cout << "Out of memory\n"; exit(1); }
strcpy(a,x); strcpy(b,y); // caller must assure space
Trang 23Client code:
char x[20]="Hello!", y[20]="Hi, there!"; int a=5, b=10;
swap(a,b); // general template function is instantiated
swap(x,y); // specialized template function is instantiated
The compiler first searches for a non-template function; if one is found and parameters match exactly, templates are not considered; if more than one non-template alternative matches, it is a syntax error
If no matching non-template alternative is found, then templates are examined If there is an exact match and its instantiation already exists, it is used and no new object code is generated; otherwise, the function is instantiated; if more than one match is found, it is an error
If no matching template function is found, non-template functions are examined using implicit conversions and promotions of arguments
A template function cannot be called by or be passed as an argument to a non-template function
Summary
In this chapter, we looked at a powerful tool for code reuse: C++ templates The underlying idea is very simple and attractive: If the algorithms should be the same for different types, you should write it only once and later indicate for what actual type you want this algorithm to be used
This is the ideal, but the practical use of this idea faces a number of difficulties The syntax of C++ templates is complex The use of specializations complicates matters even more Sometimes
figuring out which specialization will be called in each case becomes a chore Sometimes, what works on one machine under one compiler will not work on another machine under a different compiler
In addition, the use of templates entails space and performance penalty This is why many C++ programmers try to avoid templates On the other hand, templates are utilized in the Standard
Template Library (STL), and you have to understand the basic principles of using templates to be able to handle the STL library correctly
It is a powerful tool Use it with care
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 24Chapter 18 Programming with Exceptions
Topics in this Chapter
ϒΠ A Simple Example of Exception Processing
ϒΠ Syntax of C++ Exceptions
ϒΠ Exceptions with Class Objects
ϒΠ Type Cast Operators
ϒΠ Summary
In this chapter, I will deal with a relatively new C++ topic: programming with exceptions
Exception is a language mechanism that allows the programmer to separate source code that
describes the main case of processing from source code that describes exceptional situations
Exceptional situations are situations that should not occur during normal processing but from time
to time do occur Separating this exception processing from the main case makes the main case easier to read and to maintain It also makes exceptional cases easier to read and to maintain
This definition is somewhat vague, is it not? It does leave room for interpretation Indeed, what some people view as an exceptional or abnormal situation, other people perceive as a genuine part
of system operations For example, when you allocate memory on the heap, the algorithm should describe what happens if the request is satisfied Since it is possible that the computer runs out of memory, the algorithm should also specify what happens when memory is not available Is running out of memory an exception? Most people would say yes
Similarly, when the program reads data interactively from the online user, the algorithm specifies the processing of valid data What happens if the user makes a mistake and inputs data that is
invalid? Is this an exception? Most people would say no, online mistakes are a way of life, and the algorithms for processing these mistakes should be viewed as part of basic system functionality, not something that happens only rarely
Similarly, when you read data from a file in a loop, the algorithm specifies what happens when the next record is read¡Xhow different parts of the record should be processed Since it is possible that there are no more records in the file, the algorithm should define what happens when there are no more records to read Is reaching the end-of-file indeed an exception? Most people would say no, this is an event that marks the end of one stage of processing (reading file records in) and the
beginning of the next stage of processing (computations on data in memory)
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 25Regardless of whether the programmer perceives the situation as mainline processing with some additional exceptional cases (the first example) or as a set of different cases of approximately equal importance (the second and the third examples), the issue of clogging the source code with diverse computational tasks is both real and important.
To be able to make intelligent decisions on structuring your algorithms, you should have a good understanding of the tools available in the programming language This is why I will try first and foremost to explain what exceptions (as a C++ programming technique) are, what syntax they
impose on the programmer, how to use them correctly, and what incorrect uses you should try to avoid
Initially, C++ did not support exceptions and relied on C mechanisms for exception processing by using global variables accessible to the whole program (e.g., errno) or jumps and calls to special functions whose names are fixed but whose contents might be specified by the programmer (e.g.,
setjmp and longjmp)
The C++ exception facility is a relatively new language feature Similar to C++ templates, the
exception mechanism is complex The experience in using exceptions is rather limited, and the advantages of their use for system design are not demonstrated yet In addition, the use of C++ exceptions increases the program execution time and the size of the executable program This is why I do not think you should use exceptions at every opportunity that presents itself Eventually, however, they should become a legitimate part of your programming toolbox
A Simple Example of Exception Processing
Usually, processing algorithms use C++ flow control statements, mostly the if or switch
statements, to separate normal processing of data from processing of erroneous or faulty data For multistep algorithms, the segment of source code for main algorithm and for exceptional conditions are written in alternative branches of the same source code, and this often makes code harder to read¡Xthe main line is lost in the multitude of exceptional and rare cases
When something goes wrong in a function, the function might not know what to do about the error Aborting the program might be a good solution in many situations, for example, trying to push an item onto a system stack that turns out to be full On the other hand, aborting the program might not release resources held by the program, such as opened files or database locks
Another approach is setting an error code or returning an error value for the caller to check and to take a recovery action if this action is possible For example, when the client code makes an
attempt to pop an item from an empty stack, returning an error value might be an attractive
alternative However, this is not always feasible If any return value of the given type is legal for the pop function, there may be no special value to be returned to signal an exceptional condition to
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 26Setting a global variable, e.g., errno, to indicate an error does not work for concurrent programs
It is also hard to implement consistently for sequential programs because it requires that the client code diligently check the value of the global variable These checks clog the client code and make
it more difficult to understand
By using library functions such as setjmp and longjmp, the program can transfer control to an action that would release external resources and perform error recovery, but this would unwind the stack without calling destructors for objects created on the stack before these functions were called Hence, the resources held by these objects might not be properly released
Let us consider a simple example and review the issues that exception processing techniques should resolve Listing 18.1 shows a program that interactively prompts the user for the values of a
numerator and denominator of the fraction and computes and prints the fraction's value To
compute the result, the program uses two server functions, inverse() and fraction(). The first function returns the inverse of its argument It is called by the second function, fraction(), which multiplies its first argument by the value returned by inverse().
This is, of course, a somewhat convoluted design for such a simple computational problem A
simpler design would not let me demonstrate different options of exception processing A more complex problem would justify a more complex design but would drown me (and you) in a mire of details
In this problem, the zero value of the denominator is not acceptable and is rejected with a message
A negative value of the denominator is not acceptable either: If the fraction is negative, it is the numerator that should be made negative The negative denominator value should be rejected with a message that also prints the offending value
The input loop continues until the user enters a letter instead of numeric input data The cout
statement returns zero, and the break statement terminates the loop The sample output of the
program is shown in Figure 18-1
Figure 18-1 Output for program in Listing 18.1
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 27Example 18.1 Example of a program with error processing in the client code.
#include <iostream>
using namespace std;
inline void inverse(long value, double& answer)
{ answer = 1.0/value; } // answer = 1/value
inline void fraction (long numer,long denom,double& result)
{ inverse(denom, result); // result = 1.0 / denom
result = numer * result; } // result = numer/denom
int main()
{
while (true) // infinite loop
{ long numer, denom; double ans; // numerator and denominator cout << "Enter numerator and positive\n"
<< "denominator (any letter to quit): ";
if ((cin >> numer >> denom) == 0) break; // enter data
if (denom > 0) { // correct input
fraction(numer,denom,ans); // compute result
cout << "Value of the fraction: " << ans <<"\n\n";
}
else if (denom == 0) // invalid result
cout << "\nZero denominator is not allowed\n\n";
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 28Error recovery here is done by printing an error message and repeating the request for the next input data The mainline code (a call to the fraction() server function) here is not separated from error-processing code, but it does not result in serious problems.
Often, an error can be discovered only after some processing, in the server code, far from the place where the error actually originated Some of these errors could be processed at the place of their discovery, but some might require additional knowledge that might be absent in the server function that discovered the error In this case, the information about the error should be passed back to the client code for processing and, if possible, recovery I will model such a situation by moving the test of input data from client code to the server function inverse().
Listing 18.2 demonstrates this approach to error processing Function inverse() computes the inverse of its argument If the value of the argument is zero, inverse() uses the DBL_MAX constant (defined in the header file cfloat or float.h) as the inverse value Then it checks the answer to determine the validity of the result and tells the caller what happened during the call
If the answer is DBL_MAX, the inverse() function processes the error by printing an error message and returning the zero value to tell the caller about it If the argument is negative, the inverse()
function returns its value¡Xthe client will figure that out and will process the error Otherwise,
inverse() returns 1, and this will tell the caller that the value of the formal argument answer is valid
Function fraction() evaluates the return value of inverse(). If this value is 1 (the valid result),
it computes the value of the fraction If the returned value is negative (a negative denominator), it passes this value to its own client and sends to the client additional data for error processing (the message to be printed) The client code evaluates the return value of fraction(). If it is 1, the results are valid, and the main function displays the result If the return value of fraction() is negative, the client code prints this value and the message it received from fraction().
Otherwise, the client code does not do anything because the error (zero denominator) was already processed in inverse(). The result of a sample run of the program in Listing 18.2 is shown in
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 29Figure 18-2.
Figure 18-2 Output for program in Listing 18.2
Example 18.2 Example of a program with errors discovered by the server code.
#include <iostream>
#include <cfloat>
using namespace std;
inline long inverse(long value, double& answer)
{ answer = (value) ? 1.0/value : DBL_MAX;
if (answer==DBL_MAX)
{ cout << "\nZero denominator is not allowed\n\n";
return 0; } // zero denominator else if (value < 0)
{ return value; } // negative denominator else
return 1; } // valid denominator
inline long fraction (long n,long d,double& result,char* &msg)
{ long ret = inverse(d, result); // result = 1.0 / d
if (ret == 1) // valid denominator { result = n * result; } // result = n / d
if (ret < 0)
msg = "\nNegative denominator: ";
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 30cout << "Enter numerator and positive\n"
<< "denominator (any letter to quit): ";
if ((cin >> numer >> denom) == 0) break; // enter data
ret = fraction(numer,denom,ans,msg); // compute answer
if (ret == 1) // valid answer
cout << "Value of the fraction: " << ans <<"\n\n";
dependent on each other The client code has to abide by complex conventions on return values (in this example, returning 1 denotes the valid argument value, returning zero or a negative number denotes an invalid argument value) and behave differently for different return values This makes the client code more complex and requires additional documentation so that client programmers and server programmers use common conventions successfully
Another problem with this approach is that the server code, in this example, the inverse() and
fraction() functions, is involved not only in error discovery but also in communications with the user about the causes of the error For this simple example of three functions this is probably not a grave problem In more complex programs, it is important to make sure that each function performs one function (pun intended) only The function that computes the inverse of its argument should know how to compute the inverse of its argument and should not get into the user interface The function that is responsible for the user interface should know what to tell the user and should not
be involved in other computations These responsibilities have to be separated
Yet another problem with this approach is that the components of the user interface are spread all over the code of the program When the program has to be repackaged into French, Spanish,
Russian, or another language, there is no specific place in the program that has to be changed¡Xall program source code has to be inspected and modified This is asking for trouble
Listing 18.3 represents an attempt to eliminate the last two drawbacks It also gives you an
additional example of using static data members and static member functions All output strings used by the program are moved into a class, MSG, as a private static array of strings The class
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 31provides a public static function, msg(), whose argument indicates the index of the string to be used If the index is incorrect, an error message is produced instead of the expected information.
You see that the server functions are not involved in the user interface anymore The code that analyzes the situation, unfortunately, stays, but there was little that could be done about this If the code is required to discover an error, the code should test some relevant values, and that makes the code more obscure
You also see that all the components of the user interface are swept into one place This facilitates not only the adaptation of the program to other languages but also maintenance of the user interface
in general If a prompt to a user has to be changed, it is only class MSG that changes If a message has to be added or removed, the static array MSG::data[] is edited, and the number of array
components in the MSG::msg() method changes accordingly To avoid this change, the number of components in the array (defined as local in msg()) can be computed as
keeping it as a literal value is not dangerous
Notice the elements of the utilization of static data and methods: the keyword static,
initialization of data outside the class boundaries, the use of the class name in the initialization statement and in the calls to the static function, the absence of object class MSG in the application, the lack of name conflict between the function MSG::msg(), and a local variable msg in the client code
The output of this version of the program is the same as the output for two previous versions of the application This is why it is not shown again
Example 18.3 Example of extensive communications between the client and the server code.
else
return data[n]; } // return valid string } ;
char* MSG::data [] = { "\nBad argument to msg()\n",
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 32"\nZero denominator is not allowed\n\n", // depository of text
"\nNegative denominator: ",
"Enter numerator and positive\n",
"denominator (any letter to quit): ",
"Value of the fraction: "
} ;
inline long inverse(long value, double& answer, char* &msg)
{ answer = (value) ? 1.0/value : DBL_MAX;
return 1; } // valid denominator
inline long fraction (long n,long d,double& result,char* &msg)
{ long ret = inverse(d, result,msg); // result = 1.0 / d
if (ret == 1) // valid denominator
{ result = n * result; } // result = n / d
cout << MSG::msg(3) << MSG::msg(4); // prompt user for data
if ((cin >> numer >> denom) == 0) break; // enter data
ret = fraction(numer,denom,ans,msg); // compute answer
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 33both its parameter msg and its return value to communicate with its caller (and its caller's caller).
It is the use of extra parameters, return values, and complex calling conventions that C++
exceptions help to eliminate
Syntax of C++ Exceptions
C++ exceptions allow the programmer to change the flow of control when some event occurs, for example, an error These errors happen at run time (file is not found, index is invalid, etc.); when C++ raises an exception, the program may terminate if it has no handler that knows how to deal with that exception
Exception handlers are segments of program source code that should be executed when the
exception is raised, for example, printing a message to the user, collecting information for the
analysis of the causes of the exception, or error recovery
Organizing error-related source code into exception handlers can make flow of control more
logical; instead of doing all the tests in the mainline of the algorithm and thus obscuring its
meaning, error handling is coded in a separate place The tradeoff of this approach is the possibility
of obscuring the meaning of server functions that are involved in error discovery
This separate place for error recovery can be in the same method that caused the exception, in a caller of that method, in a caller of the caller, and so on This flexibility makes the design with exceptions more difficult However, the exception mechanism allows the programmer to transfer control to recovery actions in a disciplined way
Presumably, C++ exceptions allow the programmer to isolate exceptional cases in other, remote segments of source code and streamline the base processing It should make the program more readable so that it is easier to understand I am not sure that this is often the case As I mentioned earlier, the utility of using exceptions is in eliminating extra parameters, return values, and complex calling conventions among the functions that discover the problem and the functions that try to recover from the problem
When C++ raises or "throws" an exception, it can create an object of a predefined class Exception
or of a programmer-defined class This programmer-defined class can be derived from class
Exception, or it can be an independent class Again, this flexibility makes designing with
exceptions more open ended and hence more difficult to understand
The exception might be thrown explicitly with the throw statement or implicitly as the result of an illegal or invalid action The exception is caught with the catch statement, and control is
transferred to the statement that caught the exception The catch statement (or block of statements)
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 34performs the error recovery (if any) The return of control after the error recovery in the catch block depends on the structure of the program In general, it does not return by itself to the place where the exception occurred This is why the programmer has to structure the program in a specific way
if such return (e.g., to continue processing) is desirable
There are three operations related to exception handling:
ϒΠ throwing an exception
ϒΠ catching an exception
ϒΠ claiming an exception
Throwing an exception means specifying that certain exceptional (possibly erroneous) conditions
are discovered and that they should be processed using the C++ exception mechanism rather than common control flow techniques
Catching an exception means specifying a segment of code that is designed to respond to a
particular exceptional condition but not to other conditions
Claiming an exception means specifying what exception can be raised within this method; it helps
the compiler (and the client programmer and the maintenance programmer) know what to expect from the function and how it should be used
Throwing an Exception
To raise an exception, the keyword throw is used Its usage indicates that the server code has
discovered a condition that it does not know how to handle, and it throws the exception in the hope that someplace (among its client or its client's clients) there will be a segment of code that knows how to handle the situation
The keyword throw is used in a throw statement Its general syntactic form includes the keyword
throw with an operand that can be a value of any type to be thrown in search of the exception
Trang 35executes the throw statement to notify the client of the problem discovered in the server code.
The throw statement can take only a single operand of any type However, some compilers do not flag the throw statement as an error if you try to throw more than one value The value of the
throw operand is used by the client code (that tries to process the exception) to retrieve information about the context of the error Often, this information is used to define the client code behavior in error recovery
Here is a modified example of the function inverse(). In Listing 18.3, this function sets up the return values or parameter values to communicate with the client code In this version, the function
inverse() throws exceptions in two cases: (1) if it discovers that the denominator is zero and (2) if
it discovers that the denominator is negative
inline void inverse(long value,double& answer) // two parameters
{ answer = (value) ? 1.0/value : DBL_MAX;
if both throw statements threw the values of the same type If both exceptions have to be processed the same way, this is not a problem If the exceptions have to be processed differently, the client code would have to figure out what really occurred in the server code that threw the exception
If you compare this function inverse() with its version in Listing 18.1, you will see that their interfaces are similar: Both functions return a void type and have only two parameters In Listing 18.1, the function inverse() did not try to discover any exceptions Neither did its client
fraction(). It was the job of the main() client code to discover both exceptions (zero
denominator, negative denominator) and process them
In Listing 18.2 and Listing 18.3, functions inverse() and fractions() were trying to discover exceptions, recover from some of them (zero denominator) and let the main() client recover from others (negative denominator) The result was greater coupling and more confusing code The last version of inverse() throws both exceptions It does have some analysis code (to decide what exception to throw, if any), but its interface is as simple as in the first version in Listing 18.1 This simplicity will be paid for by the additional code I will write to catch and to claim these exceptions
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 36Catching an Exception
The function inverse(), which can throw two exceptions, has a direct client: function
fraction() that calls inverse() and an indirect client: function main() that calls fraction().
In general, the hierarchy of calls can be arbitrarily high If a function, in this example inverse(),
throws an exception and does not process this exception itself, one of its callers (direct or indirect) has to catch this exception
Catching an exception is a process of finding the code that can handle the error (the exception
handler); this is done by searching through a chain of function calls
One might think that catching an exception requires the keyword catch. This is true: C++ does have the keyword catch, and this keyword is used in catching the exception But this is not
enough When a function catches exceptions, it cannot catch them from an arbitrary source of
exceptions The function has to indicate from what segment of its code it will try to catch
exceptions This requires the use of yet another C++ keyword: try. This keyword should be
followed by a block that can throw exceptions
The client code that has the responsibility of catching errors encloses the code that can raise the
exception in a try statement
void foo() // function that catches exceptions
{ try // the try statement
{ statements; } // statements that throw exceptions
¡K} // the rest of foo() with catch blocks
C++ exception handlers are implemented using the keywords try and catch; the statements (or method calls) that may throw exceptions are put in the try blocks; the exception handlers
themselves are put into the catch blocks
Syntactically, one or more catch blocks should follow a try block Each catch block has a parameter
of the type that corresponds to the exception that this catch block handles
void foo() // function that catches exceptions
{ try
{ statements; } // statements that throw exceptions
catch (Type1 t1) // catch block for thrown type Type1
{ handler_for_Type1(); }
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 37catch (Type2 t2) // catch block for thrown type Type2
The try statement must be followed by at least one catch construct (block), which provides
exception handling It is an error to use a catch block that is not preceded by a try statement (It is all right if there are other catch blocks between this one and the preceding try statement.) It is an error to use a try statement that is not followed by a catch block or blocks
Recall that the throw statement has an argument of some type¡Xa character array, a long value, or even a value of some programmer-defined class type The value of the argument usually carries some data about the context of the error In the case of the inverse() function, this data is either a string with the message to be printed or the negative denominator value to be displayed If the
throw statement throws an object of a class type, the constructor for that type should enable the object to carry some information about the problem This information might be used by the catch construct for diagnostics and error recovery
If there are several catch constructs after the try block, they have to have arguments of different types Since catch constructs do not have names, the signatures of these constructs must be unique
If the exception type thrown in the try block "matches" the argument of a catch construct, the code
in the catch construct is executed and the search stops; when the catch block terminates, the
statements that follow the catch blocks for this try statement are executed
"Matching the argument" means that the exception object that is thrown by the try block can be assigned to the parameter of the catch block, meaning the exact match, any of the standard
conversions, or any of subclasses of the parameter of the catch construct For example, a double
value can be caught by a catch block with a long parameter, and a SavingsAccount object can be caught by a catch block with an Account parameter
After the catch block terminates, the statements that follow the try block and its catch constructs are executed These statements can have other try blocks (followed by catch constructs) if necessary If the try statement did not throw any exception, the catch constructs are treated as the null
statements¡Xthey are skipped
If the exception was thrown in the middle of the try statement, the execution of the try statement is terminated, the catch construct is found and executed, and so on; the statements in the try block that follow the one that threw the exception are never executed Usually, this is only logical: The
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 38exception was thrown because these statements could not be executed.
What happens if the code in the try block throws an exception that does not have a catch construct
of the appropriate type? Too bad¡Xthe function is terminated: Not only is the try block not executed
in its entirety, but the code that follows the catch constructs is not executed either This means that the appropriate catch block will be searched for in the client code of the function If it is found, all
is fine and well If the catch construct capable of handling the exception is not found even in
main(), the program is terminated
Let us consider, for example, the following version of the inverse() function that throws the
exceptions and tries to catch them
inline void inverse(long value,double&answer) // two parameters
{ try // start of try block
{ if (value == 0) // zero denominator
{ cout << str; } // zero denominator
catch (long val)
{ cout << MSG::msg(2) << val << "\n\n"; }} // negative value
If the first argument has a legitimate value, the try block is executed completely, and the catch
blocks are skipped There are no statements following the catch blocks, so the function terminates
as if it had no exception handling at all
If the first argument is zero, the character array exception is thrown and the first catch block is executed Notice that the catch block is a "block"¡Xit has its own scope, and it refers to its
parameter str rather than to the variable that has actually been thrown: MSG::msg(1).
Similarly, if the first argument is negative, its value is thrown, and the second catch block is
executed Again, the name of the value to be printed is val rather than value. No matter what exception is thrown, the statement answer = 1.0/value is never executed This is reasonable because this statement should be executed only if the value passes all the tests
If the statements in the try block throw an exception that does not have a catch block to handle it in the function inverse(), the search for the exception handler continues in fraction() and then in
main().
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 39In this version of the inverse() function, the throw statements and the catch blocks are in the same function scope Syntactically, this is legal C++ From the software engineering point of view, this is overkill¡Xthere is no need to use the exception handling mechanism if the information about exception is not passed across different functions In this case, a simple if statement within
inverse() would give the same results
Another problem with this version of exception handling is related to the execution of the rest of the program After the function inverse() terminates, its callers, fraction() and main(), have
no idea whether any exceptions were raised Meanwhile, if any exception was raised, the statement that computes the answer was not executed, and callers of inverse() should know about that
Next, let us again consider the version of inverse() that throws exceptions but does not catch them
inline void inverse(long value,double& answer) // two parameters
{ answer = (value) ? 1.0/value : DBL_MAX;
inverse(denom, result); // result = 1.0 / denom
result = numer * result; } // result = numer / denom
catch (char* str)
{ cout << str; } // zero denominator
catch (long val)
{ cout << MSG::msg(2) << val << "\n\n"; }} // negative value
This is not better than the previous version of inverse(). The exceptions should be processed in such places in the client code where the information about the exception can be used to change the behavior of the program, in this case, skipping the display of the result of computations
Listing 18.4 demonstrates this example with exception handling in the main() function As I noted earlier, this example is somewhat artificial because main() could discover that the input is invalid
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com
Trang 40itself Assuming it cannot do that, this scheme of exception handling makes sense: inverse()
discovers the error and sends information to main() so that main() could skip the use of invalid results
In Listing 18.4, function inverse() analyzes the situation and throws two expressions for the
benefit of its callers Its immediate caller, fraction(), does not have any exception handlers
(catch constructs) because it is in the function main() where the statement to be skipped is located Since fraction does not have any catch constructs, it does not have a try statement either because it would be illegal to have a try statement without catch constructs
If inverse() does not throw exceptions, fraction() and main() continue to compute and to print the result and to request the next set of data If inverse() throws an exception, it is not processed
in inverse() because it does not have appropriate catch constructs The search propagates to
fraction(). Since fraction() does not have any exception handlers, the search propagates to
main(). If main() does not have any exception handlers either, the program terminates
When the search percolates to main(), it finds there both the try statement and the catch
constructs From the point of view of main(), it is its server function fraction() that is the
source of trouble The client main() does not care whether fraction() received the exception from one of its servers or threw it itself If fraction() throws an exception, the execution of the try block is terminated before the answer is displayed The corresponding exception handler prints
a message that uses the information generated in inverse().
Example 18.4 Example of throwing and catching exceptions.
static char* msg(int n) // public static method
{ if (n<1 || n > 5) // check index validity
return data[0];
else
return data[n]; } // return valid string
} ;
char* MSG::data [] = { "\nBad argument to msg()\n",
"\nZero denominator is not allowed\n\n", // depository of text
"\nNegative denominator: ",
"Enter numerator and positive\n",
"denominator (any letter to quit): ",
Simpo PDF Merge and Split Unregistered Version - http://www.simpopdf.com