Exception handling is the built-in error handling mechanism of C++ for runtime errors. the keywords involved are: try, catch and throw.
Program statements that you want to monitor for exceptions are contained in a try block. If an exception occurs within the block, it is thrown (using throw). The exception is caught (using catch) and then processed. The exception handling part of C++ is a lot more functional and elegant than how errors are handled in C by errno.
try {
// try block
}
catch (type1 arg) {
// catch block 1
}
catch (type2 arg) {
// catch block 2
}
catch (type3 arg) {
// catch block 3
}
...
Obviously, for each try there can be more than one catch statement:
Look at the following for a toy example.
int main()
{
try {
cout << "inside try block\n";
throw 10;
cout << "never gets here\n";
}
catch (int i) {
cout << "caught exception. no. "<< i<
Note the following.
- after catch, the program execution continues, however usually exit(), abort(), etc.
are called from within the catch block.
- if in the above, "throw 10.0" is called instead, this exception is not handled,
and leads to abnormal program termination from within the catch block.
- an exception thrown from within a function called from the try block will be caught
by the catch block.
- a try block can be localized to a function. each time the function is entered, the
exception handling relative to that function is reset.
You may want catch to get all exceptions instead of just a certain type.
To do so, just use:
catch (...) {
// process all exceptions
}
A good use is to put this form as the last catch statement in a sequence,
i.e. this form becomes the default (not the original default terminate()).
To define a throw clause to the function definition, use:
ret-type func-name(arg-list) throw (type-list)
{
// ...
}
Note data types in the comma-separated type-list can be thrown by the function
- throwing any other type of expression will cause abnormal program termination
- Standard C++, this causes unexpected() to be called, which then calls terminate()
So, if you say, "throw ()", then this function cannot throw any exceptions
Furthermore, note that code in a catch block can also throw an exception.
In this case, an outer try/catch sequence will catch this exception
Now let's try to catch a few real exceptions.
The first one is new.
Over the years, new has been implemented in many different ways,
especially considering when problems happens.
In the current standard C++, new may fail and in fact throws an exception, bad_alloc.
The convoluted mess are, however, at one time this exception was called xalloc.
Additionally, the commonly assumed returning NULL is not a reliable test any more.
If you don't catch this exception, your program will be terminated. To catch this
exception, need to:
#include < new >
#include < iostream >
using namespace std;
int main()
{
int *p;
try {
p = new int;
}
catch (bad_alloc xa) {
cout << "cannot allocate\n";
return 1;
}
delete p;
}
IMPORTANT: In standard C++, there is also a form of new that returns NULL
on failure, instead of throwing an exception. That form is new(nothrow):
double * p;
p = new(nothrow) double[20000];
if (p == NULL)
...
Run-time type idientification (RTTI)
To obtain an object's type, use typeid:
#include < typeinfo >
typeid(object);
The type_info class defines the following public members:
bool operator== (const type_info &obj);
bool operator!= (const type_info &obj);
bool before(const type_info &obj); // mostly for internal purpose
const char *name(); // returns a pointer to the name of the type
typeid(type-name); // this returns a type_info boject describing the specified type,
// for use in a type comparison statement
typeid throws a bad_typeid exception, when it gets a bad pointer like:
typeid (*p); // when p == NULL
A more useful example is:
#include < iostream >
using namespace std;
class base {
};
class derived : public base {
};
int main()
{
int i;
base * p;
derived d;
p = &d;
cout << "i is " << typeid(i).name() << endl;
cout << "*p is " << typeid(*p).name() << endl;
cout << "p is " << typeid(p).name() << endl;
cout << "d is " << typeid(d).name() << endl;
return 0;
}
The output on my Mac laptop (g++ compiler) is:
i is i
*p is 4base
p is P4base
d is 7derived
A function can take a reference to a polymorphic base class. it can really get any object
derived fromt he base class. using typeid, you can find out what did you really get:
void whattype(base & ob1, base & ob2)
{
cout<< typeid(ob1).name() << endl;
if (typeid(ob1) == typeid(ob2))
cout << "yes, they match\n";
if (typeid(ob1) != typeid(base))
cout << "no, it's not the base class\n";
}
typeid can be applied to objects of a template class as well, something like:
if (typeid(*p1) == typeid(Square < double >))
cout << "yeah, got a double precision square\n";
Type Casting
C++ supports type casting operators defined by C, however, the proper C++ type casting
are:
dynamic_cast
const_cast
reinterpret_cast
static_cast
dynamic_cast tests whether a cast is valid, if not the cast fails:
- if it's on reference types --> throw a bad_cast exception
- if it involves pointers --> dynamic_cast evalutes to NULL
Here is one example:
bast * bp;
derived * dp, d;
bp = &d;
dp = dynamic_cast< derived * > (bp); // dynamic_cast < target-type > (expr)
if (!dp)
// failed
dynamic_cast can also be used with template class:
p2 = dynamic_cast< Square < double > * > (p1);
The other three casting operators are listed here as well.
const_cast is to explicitly override const and/or volatile keywords in a cast
- the target type and the source type must be the same
- it only overrides const or volatile attributes, e.g. to remove const-ness
static_cast performs a non-polymorphic cast //essentially the same as C type casting
- it can be used to cast a base class pointer into a derived class pointer
- no runtime checks are performed
reinterpret_cast changes one pointer type into another fundamentally different
pointer type.
- it can also change a pointer into an integer and an int into a pointer
- used for casting inherently incompatible pointer types
int i;
char *p;
i = reinterpret_cast< int > (p);
void func (const int * p)
{
int * v;
v = const_cast< int * > (p);
*v = 100;
}
int i;
float f;
i = static_cast< int > (f);
STL
At the core of STL are three foundational items: containers, algorithms and iterators.
Let's talk about them in turn. Note, here our purpose is mainly just to know how they
are used (quite straightforward actually).
containers are objects that hold other objects, e.g. vector, queue and list classes.
Containers are the STL objects that actually store data. There are also associative containers
for efficient retrieval of values based on keys, e.g. map class. Each container defines a set of
functions:
- insert, delete, merge for list
- push, pop for stack
algorithms act on containers, e.g. initializing, sorting searching and transforming
the contents of containers.
iterators work more or less like pointers to cycle through the contents of a container.
As there are several kinds of access that need to be supported, there are actually different
kinds of iterators:
- random access: stores and retrieves values. elements can be accessed randomly: RandIter
- bidirectional: stores and retrieves values, forward and backward moving: Bilter
- forward: stores and retrieves values, forward moving only: ForIter
- input: retrieves but does not store values. forward moving only: InIter
- output: stores by does not retrieve values. forward moving only: OutIter
iterators are handled like pointers, one can increment or decrement them. But do
note that STL also has reverse iterators, for which incrementing means backword moving.
iterators are declared using the iterator type defined by the various containers.
STL also relies on several standard components for support. When you initialize an
STL object, chances are that you may need to pick, for each of the three components,
what to use.
- allocators: each container has one defined, to manage memory allocation
you can define your own, but the default is usually sufficient
- predicates: a special function (unary or binary) returning true/false.
the programmer specifies the condition.
- comparison functions: a special BinPred, if arg1 < arg2
The container classes in STL include:
bitset: a set of bits, < bitset >
deque: a double-ended queue < deque >
list: a linear list < list >
map: key/value pairs - each key associated with only one value, < map >
multimap: key/value pairs - each key associated with >= 2 values, < map >
multiset: a set in which each element is not necessarily unique, < set >
priority_queue: a priority queue, < queue >
queue: a queue, < queue >
set: a set in which each element is unique < set >
stack: a stack < stack >
vector: a dynamic array < vector >
string: character strings < string >
STL uses templates. some placeholder types are given typedef'ed names:
size_type: int equivalent to size_t
reference: a reference to an element
const_reference: a const reference to an element
iterator: an iterator
const_iterator: a const iterator
reverse_iterator: a reverse iterator
const_reverse_iterator: a const reverse iterator
value_type: the type of a value stored in a container
allocator_type: the type of the allocator
key_type: the type of a key
key_compare: the tyep of a function that compares two keys
value_compare: the type of a functios that compares two values
Let's Learn by Example
Given our time constraints, it would probably be too overwhelming
a job for us to give a full description of how STL library is used.
Fortunately, the most commonly used functionalities used in STL
form a pretty small and self-explanary set that could be learned
by looking at a few examples. So let's examine the following ones.
Dynamic array is very useful in real world programming. In STL,
it is implemented in < vector >.
template > class vector {
// Allocator defaults to the standard allocator
};
Here is how we could use it. BTW, if you need to know all
the methods in a vector class, just google it. The reference
page is just a click away.
vector< int > v;
int i;
cout << v.size() << end;
for (i = 0; i < 10; i++) v.push_back(i);
for (i = 0; i < v.size(); i++) cout << v[i] << endl;
vector::iterator p = v.begin();
p += 2;
v.insert(p, 10, 9) // insert at element[10] a value of 9
while (p != v.end()) {
cout << *p++ << endl;
}
p -= 8;
v.erase(p, p+5); // remove the next 5 elements
You can use your own class in a vector too:
#include < iostream >
#include < vector >
using namespace std;
class demo {
double d;
public:
demo() {d = 0.;}
demo(double x) { d = x; }
demo &operator=(double x) {
d = x;
return *this;
}
double getd() { return d; }
};
bool operator< (demo a, demo b)
{
return a.getd() < b.getd();
}
bool operator==(demo a, demo b)
{
return a.getd() == b.getd();
}
int main()
{
vector < demo > v;
int i;
for (i = 0; i < 10; i ++) v.push_back(demo(i/3.0));
for (i = 0; i < v.size(); i ++) cout<< v[i].getd() << endl;
for (i = 0; i < v.size(); i ++) v[i] = v[i].getd() * 2.1;
return 0;
}
Another really useful STL class is map, for looking up a value with
a key, e.g. lookup phone numbers using names.
In map, duplicate keys are not allowed, while multimap allows
nonunique keys.
template < class Key, class T, class Comp = less< Key >, class Allocator = allocator< T > > class map {
};
Here is a short example. Note "pair" is also a class that comes
with STL. In a way, map implements a search structure for "pair".
map< char, int > m;
int i;
for (i = 0; i < 10; i ++)
m.insert(pair('A'+i,i));
for (i = 0; i < 10; i ++)
m.insert(make_pair((char)('a'+i),i));
map< char, int >::iterator p;
p = m.find('D');
if (p != m.end())
cout << p->second;
else
cout << "Key not found\n";
To hold more interesting objects in map, you just need to create
two classes: one for key and one for value, and a "<" operator
for the key type (kinda like using qsort in C).
class word {
char str[20];
public:
word() { strcpy(str,"");}
word(char *s) { strcpy(str,s); }
char *get() { return str; }
};
bool operator< (word &a, word &b)
{
return strcmp(a.get(), b.get()) < 0;
}
class opposite {
char str[20];
public:
opposite() { strcpy(str,"");}
opposite(char *s) { strcpy(str,s); }
char *get() { return str; }
};
int main()
{
map< word, opposite > m;
m.insert(pair(word("yes"),opposite("no")));
...
map< word, opposite >::iterator p;
p = m.find(word("what"));
...
}
In STL, each container provides its own basic operations.
When you need more extended and complex operations, you
then need to leverage algorithms.
The algorithms also allow working with two different types of containers
at the same time.
All the algorithms are template functions, meaning that they can be
applied to any type of container
#include < algorithm > // to access STL algorithms
Again, here we just look at one example. Whenever you find
a functionality that you need not provided by the container
class directly, first look up the reference page of STL
algorithms. There is a good chance that it's provided there.
template < class InIter, class T > size_t count(InIter start, InIter end, const T &val);
// count number of elements beginning at start, ending at end that match val
template < class InIter, class T > size_t count_if(InIter start, InIter end, UnPred pfn);
// count number of elements beginning at start, ending at end that unary
// predicate pfn returns true
These just do a count from start to end, for elements matching
a target value (count()) or meeting a criterion/prediate (count_if()).
#include < iostream >
#include < vector >
#include < algorithm >
using namespace std;
bool even(int x)
{
return !(x%2);
}
int main(void)
{
vector < int > v;
int i, n;
for (i = 0; i < 20; i ++) {
if (i%2) v.push_back(1);
else v.push_back(2);
}
n = count(v.begin(), v.end(), 0);
cout <<"count 0: "<< n << endl;
n = count(v.begin(), v.end(), 1);
cout <<"count 1: "<< n << endl;
n = count_if(v.begin(), v.end(), even);
cout <<"count even: "<< n << endl;
return 0;
}
Output:
count 0: 0
count 1: 10
count even: 10