git-svn-id: https://cppannotations.svn.sourceforge.net/svnroot/cppannotations/trunk@300 f6dd340e-d3f9-0310-b409-bdd246841980
This commit is contained in:
Frank B. Brokken 2009-11-10 16:13:47 +00:00
parent 6802f7c8b2
commit a816374567
6 changed files with 221 additions and 225 deletions

View file

@ -14,9 +14,6 @@ includefile(exceptions/example)
subsect(Exceptions: the preferred alternative)
includefile(exceptions/exception)
COMMENT(>>>>>>>>>>>>> NEXT <<<<<<<<<<<<<)
lsect(THROW)(Throwing exceptions)
includefile(exceptions/throw)
@ -26,6 +23,8 @@ includefile(exceptions/throw)
sect(The try block)
includefile(exceptions/try)
COMMENT(>>>>>>>>>>>>> NEXT <<<<<<<<<<<<<)
lsect(EXCEPTIONCATCH)(Catching exceptions)
includefile(exceptions/catch)

View file

@ -1,55 +1,67 @@
The ti(catch) block contains code that is executed when an exception is
thrown. Since em(expressions) are thrown, the tt(catch)-block must know what
kind of exceptions it should be able to handle. Therefore, the keyword
tt(catch) is followed by a parameter list consisting of but one parameter,
which is the type of the exception handled by the tt(catch)
block. So, an exception handler for tt(char const *) exceptions will have the
following form:
A ti(catch) clause consists of the keyword tt(catch) followed by a parameter
list defining one parameter specifying type and (parameter) name of the
exception the tt(catch) handler will catch. This name may then be used as a
variable in the compound statement following the tt(catch) clause.
Example:
verb(
catch (char const *message)
catch (string &message)
{
// code to handle the message
}
)
Earlier (section ref(THROW)) we've seen that such a message doesn't have
to be thrown as a static string. It's also possible for a function to return a
string, which is then thrown as an exception. If such a function
creates the string that is thrown as an exception em(dynamically), the
exception handler will normally have to delete the allocated memory to prevent
a i(memory leak).
Primitive types and objects may be thrown as exceptions. It's a
bad idea to throw a pointer or reference to a local object, but a pointer to a
em(dynamically) allocated object may be thrown as the exception handler will
be able to delete the allocated memory thus preventing a i(memory
leak). Throwing such a pointer is still dangerous as the exception handler
will not be able to distinguish dynamically allocated memory and
non-dynamically allocated memory, as illustrated by the next example:
verb(
try
{
int x;
int *xp = &x;
Close attention should be paid to the nature of the parameter of the
exception handler, to make sure that
hi(exception: dynamically generated)
dynamically generated exceptions are deleted once the handler has processed
them. Of course, when an exception is passed on to an outer level
exception handler, the received exception should em(not) be deleted by the
inner level handler.
if (condition1)
throw &xp;
Different kinds of exceptions may be thrown: tt(char *)s, tt(int)s, pointers
or references to objects, etc.: all these different types may be used in
throwing and catching exceptions. So, various types of exceptions may come out
of a tt(try)-block. In order to i(catch all expressions) that may emerge from
a tt(try)-block, multiple exception handlers (i.e., tt(catch)-blocks) may
follow the tt(try)-block.
xp = new int(0);
if (condition2)
throw xp;
}
catch (int *ptr)
{
// delete ptr or not?
}
)
Close attention should be paid to the nature of the parameter of the
exception handler, to make sure that when pointers to dynamically allocated
memory are thrown the memory is returned once the handler has processed
the pointer. In general pointers should not be thrown as exceptions. If
dynamically allocated memory must be passed to a exception handler then the
pointer should be wrapped in a smart pointer, like tt(unique_ptr) or
tt(shared_ptr) (cf. sections ref(UNIQUEPTR) and ref(SHAREDPTR)).
To some extent the em(order) hi(exception handler: order) of the exception
handlers is important. When an exception is thrown, the first exception
handler matching the type of the thrown exception is used and remaining
exception handlers are ignored. So only one exception handler following a
tt(try)-block will be executed. Normally this is no problem: the thrown
exception is of a certain type, and the correspondingly typed catch-handler
will catch it. For example, if exception handlers are defined for
tt(char *)s and ti(void *)s then ASCII-Z strings will be caught by the latter
handler. Note that a tt(char *) can also be considered a tt(void *), but even
so, an ASCII-Z string will be handled by a tt(char *) handler, and not by a
tt(void *) handler. This is true in general: handlers should be designed very
type specific to catch the correspondingly typed exception. For example,
tt(int)-exceptions are not caught by tt(double)-catchers, tt(char)-exceptions
are not caught by tt(int)-catchers. Here is a little example illustrating that
the order of the catchers is not important for types not having any
hierarchal relation to each other (i.e., tt(int) is not derived from
tt(double); tt(string) is not derived from ASCII-Z):
Multiple tt(catch) handlers may follow a tt(try) block, each handler
defining its own exception type. The em(order) hi(exception handler: order)
of the exception handlers is important. When an exception is thrown, the first
exception handler matching the type of the thrown exception is used and
remaining exception handlers are ignored. Eventually at most one exception
handler following a tt(try)-block will be used. Normally this is of no
concern as each exception has its own unique type.
Example: if exception handlers are defined for tt(char *)s and ti(void *)s
then ASCII-Z strings will be caught by the former handler. Note that a tt(char
*) can also be considered a tt(void *), but the exception type matching
procedure is smart enough to use the tt(char *) handler with the thrown
ASCII-Z string. Handlers should be designed very type specific to catch the
correspondingly typed exception. For example, tt(int)-exceptions are not
caught by tt(double)-catchers, tt(char)-exceptions are not caught by
tt(int)-catchers. Here is a little example illustrating that the order of the
catchers is not important for types not having any hierarchal relationship to
each other (i.e., tt(int) is not derived from tt(double); tt(string) is not
derived from ASCII-Z):
COMMENT(>>>>>>>>>>>>> NEXT <<<<<<<<<<<<<)
verbinclude(exceptions/examples/catchers.cc)
As an alternative to constructing different types of exception handlers
for different types of exceptions, a specific class can be designed whose

View file

@ -1,33 +1,35 @@
Situations may occur in which it is required to inspect a thrown
exception. Then, depending on the nature of the received exception, the
program may continue its normal operation, or a serious event took place,
requiring a more drastic reaction by the program. In a server-client situation
the client may enter requests to the server into a queue. Every request placed
in the queue is normally answered by the server, telling the client that the
request was successfully completed, or that some sort of error has
occurred. Actually, the server may have died, and the client should be able to
discover this calamity, by not waiting indefinitely for the server to reply.
Sometimes it is required to inspect a thrown exception. An exception catcher
may decide to ignore the exception, to process the exception, to rethrow it
after inspection or to change it into another kind of exception. For example,
in a server-client application the client may submit requests to the server by
entering them into a queue. Normally every request is eventually answered by
the server. The server may reply the request was successfully processed, or
that some sort of error has occurred. On the other hand, the server may have
died, and the client should be able to discover this calamity, by not waiting
indefinitely for the server to reply.
In this situation an intermediate i(exception handler) is called for. A thrown
In this situation an intermediate exception handler is called for. A thrown
exception is first inspected at the middle level. If possible it is processed
there. If it is not possible to process the exception at the middle level, it
is passed on, unaltered, to a more superficial level, where the really tough
exceptions are handled.
By placing an hi(throw: empty) em(empty) tt(throw) statement in the code
handling an exception the received exception is passed on to the next level
that might be able to process that particular type of exception.
By placing an hi(throw: empty) em(empty) tt(throw) statement in the exception
handler's code the received exception is passed on to the next level that
might be able to process that particular type of exception. The em(retrown)
exception is never handled by one of its neighboring exception handlers; it
is always transferred to an exception handler at a more superficial level.
In our server-client situation a function
verb(
initialExceptionHandler(char *exception)
initialExceptionHandler(string &exception)
)
could be designed to do so. The received message is inspected. If it's a
simple message it's processed, otherwise the exception is passed on to an
outer level. The implementation of tt(initialExceptionHandler()) shows the
empty tt(throw) statement:
could be designed to handle the tt(string) exception. The received message
is inspected. If it's a simple message it's processed, otherwise the exception
is passed on to an outer level. In tt(initialExceptionHandler)'s
implementation the empty tt(throw) statement is used:
verb(
void initialExceptionHandler(char *exception)
void initialExceptionHandler(string &exception)
{
if (!plainMessage(exception))
throw;
@ -35,63 +37,57 @@ empty tt(throw) statement:
handleTheMessage(exception);
}
)
As we will see below (section ref(EXCEPTIONCATCH)), the empty tt(throw)
statement passes on the exception received in a tt(catch)-block. Therefore, a
function like tt(initialExceptionHandler()) can be used for a variety of
thrown exceptions, as long as the argument used with
tt(initialExceptionHandler()) is compatible with the nature of the received
exception.
Below (section ref(EXCEPTIONCATCH)), the empty tt(throw) statement is used
to pass on the exception received by a tt(catch)-block. Therefore, a function
like tt(initialExceptionHandler) can be used for a variety of thrown
exceptions, as long as their types match tt(initialExceptionHandler)'s
parameter, which is a string.
Does this sound intriguing? Then try to follow the next example, which jumps
slightly ahead to the topics covered in chapter ref(POLYMORPHISM). The next
example may be skipped, though, without loss of continuity.
The next example jumps slightly ahead, using some of the topics covered in
chapter ref(POLYMORPHISM). The example may be skipped, though, without
loss of continuity.
We can now state that a i(basic exception handling) class can be constructed
from which specific exceptions are derived. Suppose we have a class
tt(Exception), containing a member function tt(ExceptionType
Exception::severity()). This member function tells us (little wonder!) the
severity of a thrown exception. It might be tt(Message, Warning, Mistake,
Error) or tt(Fatal). Furthermore, depending on the severity, a thrown
exception may contain less or more information, somehow processed by a
function tt(process()). In addition to this, all exceptions have a plain-text
producing member function, e.g., ti(toString()), telling us a bit more about
the nature of the generated exception.
A basic exception handling class can be constructed from which specific
exception types are derived. Suppose we have a class tt(Exception),
containing a member function tt(ExceptionType Exception::severity). This
member function tells us (little wonder!) the severity of a thrown
exception. It might be tt(Info, Notice, Warning, Error) or tt(Fatal). The
information contained in the exception depends on its severity and is
processed by a function tt(handle). In addition, all exceptions support a
member function like ti(textMsg), returning textual information about the
exception in a tt(string).
Using i(polymorphism), tt(process()) can be made to behave differently,
depending on the nature of a thrown exception, when called through a basic
tt(Exception) pointer or reference.
By defining a polymorphic function tt(handle) it can be made to behave
differently, depending on the nature of a thrown exception, when called
from a basic tt(Exception) pointer or reference.
In this case, a program may throw any of these five types of exceptions. Let's
assume that the tt(Message) and tt(Warning) exceptions are processable by our
tt(initialExceptionHandler()). Then its code would become:
In this case, a program may throw any of these five exception types. Let's
assume that classes tt(Message) and tt(Warning) were derived from the class
tt(Exception), then the tt(handle) function matching the exception type
will automatically be called by the following exception catcher:
verb(
void initialExceptionHandler(Exception const *e)
catch(Exception &ex)
{
cout << e->toString() << endl; // show the plain-text information
cout << e.textMsg() << '\n';
if
(
e->severity() != ExceptionWarning
ex.severity() != ExceptionWarning
&&
e->severity() != ExceptionMessage
ex.severity() != ExceptionMessage
)
throw; // Pass on other types of Exceptions
throw; // Pass on other types of Exceptions
e->process(); // Process a message or a warning
delete e;
ex.handle(); // Process a message or a warning
}
)
Due to polymorphism (see chapter ref(POLYMORPHISM)), tt(e->process()) will
either process a tt(Message) or a tt(Warning). Thrown exceptions are generated
as follows:
Now anywhere in the tt(try) block preceding the exception handler
tt(Exception) objects or objects of one of its derived classes may be thrown,
that will all be caught by the above handler. E.g.,
verb(
throw new Message(<arguments>);
throw new Warning(<arguments>);
throw new Mistake(<arguments>);
throw new Error(<arguments>);
throw new Fatal(<arguments>);
throw Info();
throw Warning();
throw Notice();
throw Error();
throw Fatal();
)
All of these exceptions are processable by our
tt(initialExceptionHandler()), which may decide to pass exceptions upward for
further processing or to process exceptions itself. The polymorphic exception
class is developed further in section ref(POLYMORPHEXCEPTION).

View file

@ -1,3 +1,4 @@
//PROG
#include <iostream>
#include <string>
using namespace std;
@ -11,7 +12,7 @@
:
d_name(name)
{
cout << "Object constructor of " << d_name << "\n";
cout << "Constructor of " << d_name << "\n";
}
Object(Object const &other)
:
@ -21,13 +22,12 @@
}
~Object()
{
cout << "Object destructor of " << d_name << "\n";
cout << "Destructor of " << d_name << "\n";
}
void fun()
{
Object toThrow("'local object'");
cout << "Object fun() of " << d_name << "\n";
cout << "Calling fun of " << d_name << "\n";
throw toThrow;
}
void hello()
@ -39,7 +39,6 @@
int main()
{
Object out("'main object'");
try
{
out.fun();
@ -50,17 +49,18 @@
o.hello();
}
}
//=
/*
Generated output:
Object constructor of 'main object'
Object constructor of 'local object'
Object fun() of 'main object'
Constructor of 'main object'
Constructor of 'local object'
Calling fun of 'main object'
Copy constructor for 'local object' (copy)
Object destructor of 'local object'
Destructor of 'local object'
Copy constructor for 'local object' (copy) (copy)
Caught exception
Hello by 'local object' (copy) (copy)
Object destructor of 'local object' (copy) (copy)
Object destructor of 'local object' (copy)
Object destructor of 'main object'
Destructor of 'local object' (copy) (copy)
Destructor of 'local object' (copy)
Destructor of 'main object'
*/

View file

@ -9,103 +9,98 @@ followed by an expression, defining the thrown exception value. Example:
different for exceptions.
Objects defined locally in functions are automatically destroyed once
exceptions thrown by these functions leave these functions. However, if the
object itself is thrown, the exception catcher receives
hi(throw: copy of objects)
a em(copy) of the thrown object. This copy is constructed just before the
local object is destroyed.
exceptions thrown by these functions leave these functions. This also happens
to objects thrown as exceptions. However, just before leaving the function
context the object is copied and it is this copy that eventually reaches the
approprate tt(catch) clause.
The next example illustrates this point. Within the function
tt(Object::fun()) a local tt(Object toThrow) is created, which is thereupon
thrown as an exception. The exception is caught outside of tt(Object::fun()),
in tt(main()). At this point the thrown object doesn't actually exist anymore,
Let's first take a look at the sourcetext:
verbinclude(exceptions/examples/throw.cc)
The class tt(Object) defines several simple constructors and members. The
copy constructor is special in that it adds the text tt(" (copy)") to the
received name, to allow us to monitor the construction and destruction of
objects more closely. The member function tt(Object::fun()) generates the
exception, and throws its locally defined object. Just before the exception
the following output is generated by the program:
The following examples illustrates this process.
tt(Object::fun) defines a local tt(Object toThrow), that is
thrown as an exception. The exception is caught
in tt(main). But by then the object originally thrown doesn't exist anymore,
and tt(main) received a copy:
verbinsert(exceptions/examples/throw.cc)(PROG)
tt(Object)'s copy constructor is special in that it defines its name as
tt(" (copy)") to which the other object's name is appended. This allow us to
monitor the construction and destruction of objects more closely.
tt(Object::fun) generates an exception, and throws its locally defined
object. Just before throwing the exception the program has produced the
following output:
verb(
Object constructor of 'main object'
Object constructor of 'local object'
Object fun() of 'main object'
Constructor of 'main object'
Constructor of 'local object'
Calling fun of 'main object'
)
Now the exception is generated, resulting in the next line of output:
When the exception is generated the next line of output is produced:
verb(
Copy constructor for 'local object' (copy)
)
The tt(throw) clause receives the local object, and treats it as a value
argument: it creates a copy of the local object. Following this, the exception
is processed: the local object is destroyed, and the catcher catches an
tt(Object), again a i(value parameter). Hence, another copy is
created. Therefore, we see the following lines:
The local object is passed to tt(throw) where it is treated as a value
argument, creating a copy of tt(toThrow). This copy is thrown as the
exception, and the local tt(toThrow) object ceases to exist. The thrown
exception is now caught by the tt(catch) clause, defining an
tt(Object) value parameter. Since this is a em(value) parameter yet another
copy is created. Thus, the program writes the following text:
verb(
Object destructor of 'local object'
Destructor of 'local object'
Copy constructor for 'local object' (copy) (copy)
)
Now we are inside the catcher, which displays its message:
The tt(catch) block now displays:
verb(
Caught exception
)
followed by the calling of the tt(hello()) member of the received
object. This member also shows us that we received a em(copy of the copy of
the local object) of the tt(Object::fun()) member function:
Following this tt(o)'s tt(hello) member is called, showing us that we
indeed received a em(copy of the copy) of the original tt(toThrow) object:
verb(
Hello by 'local object' (copy) (copy)
)
Finally the program terminates, and its still living objects are now
destroyed in their reversed order of creation:
Then the program terminates and its remaining objects are now
destroyed, reversing their order of creation:
verb(
Object destructor of 'local object' (copy) (copy)
Object destructor of 'local object' (copy)
Object destructor of 'main object'
Destructor of 'local object' (copy) (copy)
Destructor of 'local object' (copy)
Destructor of 'main object'
)
If the catcher would have been implemented so as to receive a
em(reference) to an object (which you could do by using `tt(catch (Object
&o))'), then repeatedly calling the copy constructor would have been
avoided. In that case the output of the program would have been:
The copy created by the tt(catch) clause clearly is superficial. It can be
avoided by defining object em(reference parameters) in tt(catch) clauses:
`tt(catch (Object &o))'). The program now produces the following output:
verb(
Object constructor of 'main object'
Object constructor of 'local object'
Object fun() of 'main object'
Constructor of 'main object'
Constructor of 'local object'
Calling fun of 'main object'
Copy constructor for 'local object' (copy)
Object destructor of 'local object'
Destructor of 'local object'
Caught exception
Hello by 'local object' (copy)
Object destructor of 'local object' (copy)
Object destructor of 'main object'
Destructor of 'local object' (copy)
Destructor of 'main object'
)
This shows us that only a single copy of the local object has been used.
Only a single copy of tt(toThrow) was created.
Of course, it's a bad idea to hi(throw: pointer to a local object) throw a
em(pointer) to a locally defined object: the pointer is thrown, but the object
to which the pointer refers dies once the exception is thrown, and the catcher
receives a i(wild pointer). Bad news....
It's a bad idea to hi(throw: pointer) throw a em(pointer) to a locally defined
object. The pointer is thrown, but the object to which the pointer refers
ceases to exist once the exception is thrown. The catcher receives a
i(wild pointer). Bad news....
Summarizing:
Let's summarize the above findings:
itemization(
it() hi(throw: local objects) Local objects are thrown as copied
objects,
it() Pointers to local objects should not be thrown.
it() However, it is hi(throw: pointer to a dynamically generated object)
possible to throw pointers or references to em(dynamically) generated
it() Local objects are thrown as copied objects;
it() Don't throw pointers to local objects;
it() It is possible to throw pointers to em(dynamically) generated
objects. In this case one must take care that the generated object is properly
deleted when the generated exception is caught, to prevent a i(memory leak).
deleted by the exception handler to prevent a i(memory leak).
)
Exceptions hi(exceptions: when) are thrown in situations where a function
can't continue its normal task anymore, although the program is still able to
continue. Imagine a program which is an interactive calculator. The program
continuously requests expressions, which are then evaluated. In this case the
parsing of the expression may show syntactic errors; and the evaluation of
the expression may result in expressions which can't be evaluated, e.g.,
because of the expression resulting in a i(division by zero). Also, the
calculator might allow the use of variables, and the user might refer to
i(non-existing variables): plenty of reasons for exceptions to be thrown, but
no overwhelming reason to terminate the program. In the program, the following
code may be used, all throwing exceptions:
Exceptions are thrown in situations where a function can't complete its
assigned task, but the program is still able to continue. Imagine a program
offering an interactive calculator. The program expects numeric expressions,
which are evaluated. Expressions may show syntactic errors or it may be
mathematically impossible to evaluate them. Maybe the calculator allows us to
define and use variables and the user might refer to non-existing variables:
plenty of reasons for the expression evaluation to fail, and so many reasons
for exceptions to be thrown. None of those should terminate the
program. Instead, the program's user is informed about the nature of the
problem and is invited to enter another expression. Example:
verb(
if (!parse(expressionBuffer)) // parsing failed
throw "Syntax error in expression";
@ -116,14 +111,12 @@ code may be used, all throwing exceptions:
if (divisionByZero()) // unable to do division
throw "Division by zero is not defined";
)
The hi(location of throw statements) location of these tt(throw)
statements is immaterial: they may be placed deeply nested within the program,
or at a more superficial level. Furthermore, em(functions) may be used to
generate hi(throw: function return values) the expression which is then
thrown. A function
centt(char const *formatMessage(char const *fmt, ...);)
would allow us to throw more specific messages, like
Where these tt(throw) statements are located is irrelevant: they may be
found deeply nested inside the program, or at a more superficial level.
Furthermore, em(functions) may be used to generate the exception to be
thrown. An tt(Exception) object might support stream-like insertion operations
allowing us to do, e.g.,
verb(
if (!lookup(variableName))
throw formatMessage("Variable '%s' not defined", variableName);
throw Exception() << "Undefined variable '" << variableName << "';
)

View file

@ -1,30 +1,26 @@
The ti(try)-block surrounds statements in which exceptions may be thrown. As
we have seen, the actual tt(throw) statement can be placed everywhere, not
necessarily directly in the tt(try)-block. It may, for example, be placed in a
function, called from within the tt(try)-block.
The ti(try)-block surrounds tt(throw) statements. Remember that a program is
always surrounded by a global tt(try) block, so tt(throw) statements may
appear anywhere in your code. More often, though, tt(throw) statements are
used in function bodies and such functions may be called from within tt(try)
blocks.
The keyword tt(try) is followed by a set of curly braces, acting like a
standard bf(C++) i(compound statement): multiple statements and
definitions may be placed here.
It is possible (and very common) to create hi(exception: levels) em(levels) in
which exceptions may be thrown. For example, tt(main())'s code is surrounded
by a tt(try)-block, forming an outer level in which exceptions can be handled.
Within tt(main())'s tt(try)-block, functions are called which may also contain
tt(try)-blocks, forming the next level in which exceptions may be
generated. As we have seen (in section ref(EMPTYTHROW)), exceptions thrown in
A tt(try) block is defined by the keyword tt(try) followed by a compound
statement. This block, in turn, em(must) be followed by at least one
tt(catch) handler:
verb(
try
{
// any statements here
}
catch(...) // at least one catch clause here
{}
tt(Try)-blocks are commonly nested, creating exception em(levels). For
example, tt(main)'s code is surrounded by a tt(try)-block, forming an outer
level handling exceptions. Within tt(main)'s tt(try)-block functions are
called which may also contain tt(try)-blocks, forming the next exception
level. As we have seen (section ref(EMPTYTHROW)), exceptions thrown in
inner level tt(try)-blocks may or may not be processed at that level. By
placing an hi(throw: empty) empty tt(throw) in an exception handler, the
placing an empty tt(throw) statement in an exception handler, the
thrown exception is passed on to the next (outer) level.
If an exception hi(exception: outside of try block)
is thrown outside of any tt(try)-block, then the default
hi(exception: default handling) way to handle (uncaught) exceptions is
used, which is normally to i(abort) the program. Try to compile and run the
following tiny program, and see what happens:
verb(
int main()
{
throw "hello";
}
)