Exceptions completed

git-svn-id: https://cppannotations.svn.sourceforge.net/svnroot/cppannotations/trunk@311 f6dd340e-d3f9-0310-b409-bdd246841980
This commit is contained in:
Frank B. Brokken 2009-12-03 19:52:30 +00:00
parent e84e0bb7b9
commit 525d470554
3 changed files with 160 additions and 104 deletions

View file

@ -72,7 +72,6 @@ includefile(classes)
lchapter(MEMORY)(Classes And Memory Allocation)
includefile(memory)
COMMENT(>>>>>>>>>>>>> NEXT <<<<<<<<<<<<<)
COMMENT( 9 )
lchapter(EXCEPTIONS)(Exceptions)
includefile(exceptions)

View file

@ -53,9 +53,6 @@ includefile(exceptions/guarantees)
lsect(FUNTRY)(Function try blocks)
includefile(exceptions/function)
COMMENT(>>>>>>>>>>>>> NEXT <<<<<<<<<<<<<)
lsect(CONSEXCEPTIONS)(Exceptions in constructors and destructors)
includefile(exceptions/constructors)

View file

@ -1,13 +1,13 @@
Only constructed objects are eventually destroyed. Although this may sound
like a truism, there is a subtlety here. If the construction of an object
fails for some reason, the object's destructor will not be called once the
object goes out of scope. This could happen if an emi(uncaught exception)
hi(exception: uncaught)
hi(constructor: throwing exceptions)
is generated by the constructor. If the exception is thrown em(after) the
object has allocated some memory, then its destructor (as it isn't called)
won't be able to delete the allocated block of memory. A em(memory leak) will
be the result.
Only completely constructed objects are automatically destroyed by the
bf(C++) run-time system. Although this may sound like a truism, there is a
subtlety here. If the construction of an object fails for some reason, the
object's destructor will not be called once the object goes out of scope. This
could happen if an exception
hi(exception: and constructors)hi(constructor: and exceptions)
generated by the constructor is not caught by the constructor. If the
exception is thrown em(after) the object has allocated some memory, then that
memory will not be returned by the object's destructor as the destructor isn't
called because the object hasn't completely been constructed.
The following example illustrates this situation in its prototypical
form. The constructor of the class tt(Incomplete) first displays a message
@ -24,72 +24,129 @@ block. Any exception that may be generated is subsequently caught:
)
Thus, if tt(Incomplete)'s constructor would actually have allocated some
memory, the program would suffer from a memory leak. To prevent this from
happening, the following countermeasures are available:
happening, the following counter measures are available:
itemization(
it() Exceptions should not leave the constructor. If part of the
constructor's code may generate exceptions, then this part should itself be
surrounded by a tt(try) block, catching the exception within the
constructor. There may be good reasons for throwing exceptions out of the
constructor, as that is a direct way to inform the code using the constructor
that the object has not become available. But before the exception leaves the
constructor, it should be given a chance to delete memory it already has
allocated. The following skeleton setup of a constructor shows how this can be
implemented. Note how any exception that may have been generated is rethrown,
allowing external code to inspect this exception too:
it() Prevent the exceptions from leaving the constructor.nl()
If part of the constructor's body may generate exceptions, then this
part may be surrounded by a tt(try) block, allowing the exception to be caught
by the constructor itself. This is approach is defensible when the constructor
is able to repair the cause of the exception and to complete its construction
as a valid object.
it() If an exception is generated by a base class constructor or by a
member initializing constructor then a tt(try) block within the constructor's
body won't be able to catch the thrown exception. This em(always) results
in the exception leaving the constructor and the constructor will never be
considered properly constructed. A tt(try) block may include the member
initializers, and the tt(try) block's compound statement becomes the
constructor's body as in the following example:
verb(
Incomplete::Incomplete()
class Incomplete2
{
try
{
d_memory = new Type;
code_maybe_throwing_exceptions();
}
catch (...)
{
delete d_memory;
throw;
}
Composed d_composed;
public:
Incomplete2()
try
:
d_composed(/* arguments */)
{
// body
}
catch (...)
{}
};
)
it() Exceptions might be generated while initializing members. In those
cases, a tt(try) block within the constructor's body has no chance to catch
such exceptions. When a class uses pointer data members, and exceptions are
generated em(after) these pointer data members have been initialized, memory
leaks can still be avoided, though. This is accomplished by using em(smart
pointers), e.g., em(shared_ptr) objects, introduced in section
ref(SHAREDPTR). As tt(shared_ptr) objects are objects, their destructors are
still called, even when their the full construction of their composing object
fails. In this case the rule em(once an object has been constructed its
destructor is called when the object goes out of scope) still applies.
Section ref(SHAREDCONS) covers the use of tt(shared_ptr) objects to prevent
memory leaks when exceptions are thrown out of constructors, even if the
exception is generated by a member initializer.
bf(C++), however, supports an even more generic way to prevent exceptions
from leaving functions (or constructors):
emi(function try block)em(s). These function try blocks are discussed in
the next section.
An exception thrown by either the member initializers or the body will
result in the execution never reaching the body's closing curly brace. Instead
the catch clause is reached. Since the constructor's body isn't properly
completed the object is not considered properly constructed and eventually the
object's destructor won't be called.
)
Destructors have problems of their own when they generate
exceptions. Exceptions leaving destructors may of course produce memory leaks,
as not all allocated memory may already have been deleted when the exception
is generated. Other forms of incomplete handling may be encountered. For
example, a database class may store modifications of its database in memory,
leaving the update of file containing the database file to its destructor. If
the destructor generates an exception before the file has been updated, then
there will be no update. But another, far more subtle, consequence of
exceptions leaving destructors exist.
The catch clause of a constructor's function tt(try) block behaves
slightly different than a catch clause of an ordinary function tt(try)
block. An exception reaching a constructor's function tt(try) block may be
transformed into another exception (which is thrown from the catch clause) but
if no exception is explicitly thrown from the catch clause the exception
originally reaching the catch clause is always rethrown. Consequently, there's
no way to confine an exception thrown from a base class constructor or from a
member initializer to the constructor: such an exception will em(always)
propagate to a more shallow block and the object's construction will always be
considered incomplete.
The situation we're about to discuss may be compared to a carpenter
building a cupboard containing a single drawer. The cupboard is finished, and
a customer, buying the cupboard, finds that the cupboard can be used as
expected. Satisfied with the cupboard, the customer asks the carpenter to
build another cupboard, this time containing em(two) drawers. When the second
cupboard is finished, the customer takes it home and is utterly amazed when
the second cupboard completely collapses immediately after its first use.
Consequently, if incompletely constructed objects throw exceptions then
the constructor's catch clause is responsible for preventing memory
(generally: resource) leaks. There are several ways to realize this:
itemization(
it() When multiple inheritance is used: if initial base classes have
properly been constructed and a later base class throws, then the initial base
class destructors will automatically be destroyed (as they are themselves
fully constructed objects)
it() When composition is used: already constructed composed objects will
automatically be destroyed (as they are fully constructed objects)
it() Instead of using plain pointers em(smart pointers) (cf. section
ref(SHAREDPTR)) should be used to manage dynamically allocated memory. In this
case, if the constructor throws either before or after the allocation of the
dynamic memory allocated memory will properly be returned as tt(shared_ptr)
objects are objects.
it() If plain pointer em(must) be used then the constructor's body should
use local pointers when dynamically allocating memory and assign the local
pointers to the plain pointer data members in a final em(nothrow) section. For
example:
verb(
class Incomplete2
{
Composed d_composed;
char *d_cp;
int *d_ip;
Weird story? Consider the following program:
public:
Incomplete2(size_t nChars, size_t nInts)
try
:
d_composed(/* arguments */) // may throw
d_cp(0),
d_ip(0)
{
preamble(); // may throw
try
{
d_cp = new char[nChars]; // may throw
d_ip = new int[nChars]; // may throw
postamble(); // may throw
}
catch(...)
{
delete[] d_cp; // clean up
delete[] d_ip;
throw;
}
// onlly nohrow operations here
}
catch (...)
{}
};
)
)
According to the bf(C++) standard exceptions thrown by destructors may not
leave their bodies. Consequently a destructor cannot sensibly be provided with
a function tt(try) block as exceptions caught by a function tt(try) block's
catch clause has already left the destructor's body (and will be retrown as
with exceptions reaching a constructor's function tt(try) block's catch
clause).
The consequences of an exception leaving the destructor's body is not
defined, and may result in unexpected behavior. Consider the following example:
Assume a carpenter builds a cupboard containing a single drawer. The cupboard
is finished, and a customer, buying the cupboard, finds that the cupboard can
be used as expected. Satisfied with the cupboard, the customer asks the
carpenter to build another cupboard, this time containing em(two)
drawers. When the second cupboard is finished, the customer takes it home and
is utterly amazed when the second cupboard completely collapses immediately
after its first use.
Weird story? Then consider the following program:
verbinsert(MAIN)(exceptions/examples/destructor.cc)
When this program is run it produces the following output:
verb(
@ -101,10 +158,11 @@ the second cupboard completely collapses immediately after its first use.
Drawer 1 used
Abort
)
The final tt(Abort) indicating that the program has aborted, instead of
displaying a message like tt(Cupboard2 behaves as expected). Now let's have a
look at the three classes involved. The class tt(Drawer) has no particular
characteristics, except that its destructor throws an exception:
The final tt(Abort) indicates that the program has aborted instead of
displaying a message like tt(Cupboard2 behaves as expected).
Let's have a look at the three classes involved. The class tt(Drawer) has no
particular characteristics, except that its destructor throws an exception:
verbinsert(DRAWER)(exceptions/examples/destructor.cc)
The class tt(Cupboard1) has no special characteristics at all. It merely
has a single composed tt(Drawer) object:
@ -116,39 +174,38 @@ composed tt(Drawer) objects:
When tt(Cupboard1)'s destructor is called, tt(Drawer)'s destructor is
eventually called to destroy its composed object. This destructor throws an
exception, which is caught beyond the program's first tt(try) block. This
behavior is completely as expected. However, a problem occurs when
tt(Cupboard2)'s destructor is called. Of its two composed objects, the
destructor of the second tt(Drawer) is called first. This destructor throws
an exception, which ought to be caught beyond the program's second tt(try)
block. However, although the flow of control by then has left the context of
tt(Cupboard2)'s destructor, that object hasn't completely been destroyed yet
as the destructor of its other (left) tt(Drawer) still has to be
called. Normally that would not be a big problem: once the exception leaving
tt(Cupboard2)'s destructor is thrown, any remaining actions would simply be
ignored, albeit that (as both drawers are properly constructed objects)
tt(left)'s destructor would still be called. So this happens here
too. However, tt(left)'s destructor em(also) throws an exception. Since we've
already left the context of the second tt(try) block, the programmed
flow control is completely mixed up, and the program has no other option but
to abort. It does so by calling tt(terminate()), which in turn calls
tt(abort()). Here we have our collapsing cupboard having two drawers, even
though the cupboard having one drawer behaves perfectly.
behavior is completely as expected.
Now a problem occurs when tt(Cupboard2)'s destructor is called. Of its two
composed objects, the second tt(Drawer)'s destructor is called first.
This destructor throws an exception, which ought to be caught beyond the
program's second tt(try) block. However, although the flow of control by then
has left the context of tt(Cupboard2)'s destructor, that object hasn't
completely been destroyed yet as the destructor of its other (left) tt(Drawer)
still has to be called.
Normally that would not be a big problem: once an exception is thrown from
tt(Cupboard2)'s destructor any remaining actions would simply be ignored,
albeit that (as both drawers are properly constructed objects) tt(left)'s
destructor would still have to be called.
This happens here too and tt(left)'s destructor em(also) needs to throw an
exception. But as we've already left the context of the second tt(try) block,
the current flow control is now thoroughly mixed up, and the program has no
other option but to abort. It does so by calling tt(terminate()), which in
turn calls tt(abort()). Here we have our collapsing cupboard having two
drawers, even though the cupboard having one drawer behaves perfectly.
The program aborts since there are multiple composed objects whose
destructors throw exceptions leaving the destructors. In this situation one of
the composed objects would throw an exception by the time the program's flow
control has already left its proper context. This causes the program to abort.
control has already left its proper context causing the program to abort.
This situation can be prevented if we ensure that exceptions
em(never) leave destructors. In the cupboard example, tt(Drawer)'s destructor
throws an exception leaving the destructor. This should not happen: the
exception should be caught by tt(Drawer)'s destructor itself. Exceptions
should never be thrown out of destructors, as we might not be able to catch,
at an outer level, exceptions generated by destructors. As long as we view
destructors as service members performing tasks that are em(directly) related
to the object being destroyed, rather than a member on which we can base any
flow control, this should not be a serious limitation. Here is the skeleton of
a destructor whose code might throw exceptions:
The bf(C++) standard therefore understandably stipulates that exceptions
may em(never) leave destructors. Here is the skeleton of a destructor whose
hi(destructor: and exceptions)hi(exception: and destructors) code might throw
exceptions. No function tt(try) block but all the destructor's actions are
encapsulated in a tt(try) block nested under the destructor's body.
verb(
Class::~Class()
{
@ -160,3 +217,6 @@ a destructor whose code might throw exceptions:
{}
}
)