PART 7

Advanced Features

and

Class Libraries

· Templates

· Exception Handling

· Multiple Inheritance

· ANSI std Class Library


Advanced Features

“Advanced features” in C++ refer to feature ideas added on since AT&T Cfront 1.2, or, in other words, ideas not described in Bjarne Stroustrup’s 1986 edition of The C++ Programming Language.

These ideas fall along three major lines:

·  Multiple Inheritance, first implemented in AT&T 2.0 compilers

·  Function Templates, first implemented in AT&T 3.0 compilers

·  Exception Handling, first implemented in AT&T 4.0 compilers and compilers based on the ANSI standard


template.cpp

/* TEMPLATE.CPP: Test function templates */

#include <iostream.h>

template <class T> T max( T a, T b)

{

return (a > b) ? a : b ;

}

int max(int, int);

int main()

{

int x = 1, y = 2;

double u = 6.5, v = 2.5;

cout < "2 const ints " < max (5, 6) < endl;

cout < "2 varia ints " < max (x, y) < endl;

cout < "const/var int " < max (7, x) < endl;

cout < "2 var doubles " < max (u, v) < endl;

cout < "2 const longs " < max (9L, 11L) < endl;

cout < "2 const chars " < max ('F', 'a') < endl;

cout < "int & double " < max (5, u) < endl;

cout < "int & double " < max (7, u) < endl;

cout < "char and int " < max ('F', 3) < endl;

return 0;

}


TEMPLATES

·  Templates are used to relieve the tedium of writing functions for every possible combination of data types.

·  Templates are generally superior to macros because they allow type checking and function overloading.

·  Templates can get tricky when all combinations of various classes and built-in data types need to be considered.

·  The code in template.cpp uses a template instead of a macro to implement the max function. It will work fine in all cases where the data types of the two arguments are the same. The line

int max(int, int);

is needed if we wish to mix data types and expect some kind of “default” behavior for “standard” data types. In this case, with mixed data types the results will be converted to integer. Without this line the code

cout < "int & double " < max (5, u) < endl;

cout < "int & double " < max (7, u) < endl;

cout < "char and int " < max ('F',3)< endl;

would fail


temclass.h

#ifndef temclass

#define temclass

#include <iostream.h>

template <class T>

class stack

{

T* v;

T* p;

int sz;

public:

stack(int s) {v = p = new T[sz=s];}

~stack() {delete [] v;}

void push(T a) {*p++ = a;}

T pop() {return *--p;}

int size() const {return p-v;}

};

#endif


TEMPLATE CLASSES

·  The concept of templates is very general. This means that in addition to allowing templates to generate the appropriate functions, templates can also be used to generate classes as needed.

·  Frequently classes are needed to “contain” objects of another classes. These are called container classes. It would be tedious to write a different container for every object that needed a collection. One way to implement container classes is to define a data structure in a template class, and then invoke the template whenever it is desired to use that data structure to organize a collection of objects.

·  The example given uses a stack data structure to organize a collection of objects of any type (including user-defined classes). In a stack, the item most recently put into the stack (via push) is the item that will next be retrieved (via pop).

·  This program uses dynamic storage allocation. However, it does absolutely no error checking to see if space is available, if the stack has been overrun, or if the stack has been underrun.


temclass.cpp

/* TEMCLASS.CPP: Test template classes - Stroustrup */

#include <iostream.h>

#include "temclass.h"

int main()

{

stack<char> mydata(100); //stack of characters

stack<double> mynums(10); //stack of doubles

char a = 'a', b = 'b';

double u = 6.5, v = 2.5;

mydata.push(a);

mydata.push(b);

mydata.push('c');

mynums.push(u);

mynums.push(v);

mynums.push(8.7);

mynums.push(double(4));

cout < "Data from the two stacks" < endl;

cout < mynums.pop() < mydata.pop()< endl;

cout < "Remaining data in the char stack" < endl;

cout < mydata.pop() < endl;

cout < mydata.pop() < endl;

cout < "Remaining data in the float stack" < endl;

int extent = mynums.size();

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

cout < mynums.pop() < endl;

return 0;

}


TRIVIAL CONVERSIONS

FROM: / TO:
T / const T
T / T&
T& / T
T[] / T*
T(args) / (*T)(args)
T / volatile T
T* / const T*
T* / volatile T*


ARGUMENT MATCHING RULES

·  When functions are overloaded, the compiler needs to know which function to call. C++ does not require that you write a function for every possible variant of parameter type, particularly where numeric types and pointers to various parts of a class hierarchy are concerned. In addition, templates provide an “unlimited” number of function prototypes.

·  To resolve which functions are called with what parameters converted in what ways, argument matching rules are applied. The best matching function is the intersection of sets of functions that best match on each argument. If this intersection has no members or more than one member, then the call is illegal.

  1. Exact matches match first. These are matches that involve only trivial conversions. The fewer conversions the better, with conversions to const and volatile being less desirable than the rest.
  1. Next comes matches that require promotions in addition to trivial conversions. The fewer promotions the better, with promotions of a char, signed char, short int, or enum to an int; promotions of a char, unsigned char, or unsigned short to an unsigned int; and promotions of a float to a double being more desirable than the rest.
  1. Next comes matches that require standard conversions in addition to trivial conversions and promotions. These include standard arithmetic conversions (as in ANSI C) as well as pointer and reference conversions. Suppose we have class A, publicly derive class B from class A, and then publicly derive class C from class B. Converting from B* to A* is better than converting from B* to void* or converting from B* to const void*. Converting from C* to B* is better than converting from C* to A*. Likewise, converting from C& to B& is better than converting from C& to A&.
  1. Next comes matches that require user-defined conversions.
  1. Finally comes matches to an ellipsis.


assert1.cpp

/* assert1.cpp - use assert macro for error handling */

#include <iostream.h>

#include <assert.h>

void assertChk(int value);

main()

{

int x = 10, y = 20;

cout < "Before first check\n";

assertChk(x);

cout < "Between checks\n";

assertChk(y);

cout < "After checks\n";

return 0;

}

void assertChk(int value)

{

assert (value < 15);

return;

}


ASSERTIONS FOR ERROR CHECKING

·  The assert macro is a facility found in C and C++ compilers that was primarily designed for testing assertions at the beginning of functions during code development.

·  To perform assertion testing, the following steps are taken:
1. Include the header file assert.h
2. Place statements you expect to always be true inside what looks like a function call to a function named assert
3. DO NOT define the symbol NDEBUG

·  To disable assertion testing for production code, insert the line:
#define NDEBUG
just ahead of the line including the header file assert.h

·  If an assertion fails when assertion testing is enabled, the program prints out a message saying what assertion failed where, and brings the program to a total halt (with an exit(1)).

·  While this mechanism might be acceptable in code development, generally more error processing is necessary in production environments.


hello6.cpp

// HELLO6.CPP: test exception handling
#include <iostream.h>
#include <string.h>
#include <ctype.h>
int getinfo (char *, char &, int &);
void getsexcode (char &);

int main()

{

char name[20], sexcode;

int status = 1, age;

try

{

status = getinfo (name, sexcode, age);

}

catch (char* message)

{

cout < "Name error: " < message < endl;

}

catch (char gender)

{

cerr < "Sex code error: " < gender < endl;

}

catch (int howold)

{

cerr < "Age error: " < howold < endl;

}

if (status == 0)

cout < name < ", " < sexcode < ", " < age < endl;

return 0;

}


Exception Handling

·  Exception handling is necessary whenever an error of a catastrophic nature occurs while nested many levels deep in function calls, or where the called function is not sure how the caller wishes to attempt recovery from the error.

·  Historically, many designers argued that the goto statement and global variables were absolutely essential to provide adequate error recovery.

·  C provided a mechanism for exception handling using the setjmp/longjmp mechanism. However, this mechanism does not always provide the kind to stack cleanup necessary when unceremoniously exiting several levels of function calls, particularly if the functions have declared class instances that have constructors that dynamically allocate storage.

·  C++ 4.0 provides a much more robust mechanism for exception handling using the try, catch, and throw keywords.

·  The try block must be immediately followed by all of the catch blocks related to the try. Exception handling can only take place in try blocks, so if it is desired to enable exception handling throughout the program virtually the entire contents of main will be in a try block.

·  The throw blocks may occur anywhere in code executed within or called from a try block. Which catch block handles the thrown message depends on the data type of the expression thrown. The thrown message need not be the same as the return type of the function called or any of its parameters.

·  When a throw statement is executed all called functions are destroyed and the called stacks restored in an orderly manner. The only problem with that behavior is that you cannot throw back a pointer to an object dynamically created in any of the called functions.


hello6.cpp (continued)

int getinfo (char * name, char & sexcode, int & age)
{
char inname[21];

inname[19] = '\0';

cout < "What is your name? ";

cin.width(sizeof(inname));

cin > inname;

if (inname[19] != '\0')

{

inname[19] = '\0';

strcpy (name, inname);

throw name; // throw inname will NOT work

}

else

{

strcpy (name, inname);

}

getsexcode (sexcode);

cout < "What is your age? ";

cin > age;

if (age < 0)

throw age;

return 0;

}

void getsexcode (char & sexcode)

{

char response[10];

cout < "What sex are you? ";

cin.width(sizeof (response));

cin > response;

sexcode = response[0];

if (toupper(response[0]) != 'M'

& toupper(response[0]) != 'F')

throw sexcode; // throw response won't work

return;

}


triangl4.h

/* Triangle class with exception handling */

#ifndef triangl4

#define triangl4

#include <iostream.h>

class triangle

{

private:

double side1,

side2,

side3;

public:

class badshape{};

triangle (double a = 1.0, double b = 1.0,

double c = 1.0);

double perimeter ();

triangle operator + (triangle b);

void print () const;

};

// constructor for class triangle

triangle::triangle (double a, double b, double c):

side1(a), side2(b), side3(c)

{

if (side1+side2 <= side3)

throw badshape();

}

double triangle::perimeter ()

{

return ( side1 + side2 + side3);

}

triangle triangle::operator + (triangle b)

{

triangle temp;

temp.side1 = side1 + b.side1;

temp.side2 = side2 + b.side2;

temp.side3 = side3 + b.side3;

return temp;

}

void triangle::print () const

{

cout < "(" < side1 < ", " < side2 < ", "

< side3 < ")\n";

}

#endif

triangl4.cpp

// TRIANGL4.CPP: test exception handling

#include <iostream.h>

#include <string.h>

#include <ctype.h>

#include <math.h>

#include <stdlib.h>

#include "triangl4.h"

main()

{

double a;

try

{

triangle q, r(3.0,4.0,11.0), t;

const triangle s(8.0,8.0);

cout < "q = ";

q.print();

cout < "r = ";

r.print();

cout < "s = ";

s.print();

a = q.perimeter();

cout < "perimeter of q = " < a < "\n";

t = r + s;

cout < "t = ";

t.print();

}

catch (triangle::badshape)

{

cout < "Bad shape " < endl;

exit(1);

}

cout < "Success!" < endl;

return 0;

}


Multiple Inheritance

·  Object-oriented design seldom yields a complete problem solution in which the classes form a perfect tree structure with all classes inheriting from a single parent class.

·  Fortunately, C++ has support for multiple inheritance (since AT&T version 2.0). In fact, the iostream library (since AT&T version 3.0) uses multiple inheritance in its definition.

·  Conceptually, multiple inheritance can be used whenever we desire to create a class that has attributes of two existing classes.

·  Syntactically, the new class can be created similar to the way other derived classes are created, except that two parent classes are listed.

·  Existing classes, however, may need to be modified to allow the new class to inherit what it needs:

- if the derived class needs access to the private data of any parent, those data elements must be changed from private to protected

- if there are virtual functions called from the derived class that in turn calls the function with the same name in the parent class, a scope resolution operator may be needed

- if there are functions to be inherited from a common grandparent (or farther up) class, the intermediate classes may need to be declared virtual so that the derived class “sees through” the multiple immediate parents


house.h (page 1 of 4)

/* Shape, triangle, rectangle, house inheritance */

#ifndef house

#define house

#include <iostream.h>

#include <math.h>

class SHAPE

{

public:

SHAPE(void);

double operator +(SHAPE& x);

double operator *(SHAPE& x);

virtual double perimeter(void) const = 0;

virtual double area(void) const = 0;