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) lchapter(MEMORY)(Classes And Memory Allocation)
includefile(memory) includefile(memory)
COMMENT(>>>>>>>>>>>>> NEXT <<<<<<<<<<<<<)
COMMENT( 9 ) COMMENT( 9 )
lchapter(EXCEPTIONS)(Exceptions) lchapter(EXCEPTIONS)(Exceptions)
includefile(exceptions) includefile(exceptions)

View file

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

View file

@ -1,13 +1,13 @@
Only constructed objects are eventually destroyed. Although this may sound Only completely constructed objects are automatically destroyed by the
like a truism, there is a subtlety here. If the construction of an object bf(C++) run-time system. Although this may sound like a truism, there is a
fails for some reason, the object's destructor will not be called once the subtlety here. If the construction of an object fails for some reason, the
object goes out of scope. This could happen if an emi(uncaught exception) object's destructor will not be called once the object goes out of scope. This
hi(exception: uncaught) could happen if an exception
hi(constructor: throwing exceptions) hi(exception: and constructors)hi(constructor: and exceptions)
is generated by the constructor. If the exception is thrown em(after) the generated by the constructor is not caught by the constructor. If the
object has allocated some memory, then its destructor (as it isn't called) exception is thrown em(after) the object has allocated some memory, then that
won't be able to delete the allocated block of memory. A em(memory leak) will memory will not be returned by the object's destructor as the destructor isn't
be the result. called because the object hasn't completely been constructed.
The following example illustrates this situation in its prototypical The following example illustrates this situation in its prototypical
form. The constructor of the class tt(Incomplete) first displays a message 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 Thus, if tt(Incomplete)'s constructor would actually have allocated some
memory, the program would suffer from a memory leak. To prevent this from 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( itemization(
it() Exceptions should not leave the constructor. If part of the it() Prevent the exceptions from leaving the constructor.nl()
constructor's code may generate exceptions, then this part should itself be If part of the constructor's body may generate exceptions, then this
surrounded by a tt(try) block, catching the exception within the part may be surrounded by a tt(try) block, allowing the exception to be caught
constructor. There may be good reasons for throwing exceptions out of the by the constructor itself. This is approach is defensible when the constructor
constructor, as that is a direct way to inform the code using the constructor is able to repair the cause of the exception and to complete its construction
that the object has not become available. But before the exception leaves the as a valid object.
constructor, it should be given a chance to delete memory it already has it() If an exception is generated by a base class constructor or by a
allocated. The following skeleton setup of a constructor shows how this can be member initializing constructor then a tt(try) block within the constructor's
implemented. Note how any exception that may have been generated is rethrown, body won't be able to catch the thrown exception. This em(always) results
allowing external code to inspect this exception too: 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( verb(
Incomplete::Incomplete() class Incomplete2
{ {
try Composed d_composed;
{ public:
d_memory = new Type; Incomplete2()
code_maybe_throwing_exceptions(); try
} :
catch (...) d_composed(/* arguments */)
{ {
delete d_memory; // body
throw; }
} catch (...)
{}
}; };
) )
it() Exceptions might be generated while initializing members. In those An exception thrown by either the member initializers or the body will
cases, a tt(try) block within the constructor's body has no chance to catch result in the execution never reaching the body's closing curly brace. Instead
such exceptions. When a class uses pointer data members, and exceptions are the catch clause is reached. Since the constructor's body isn't properly
generated em(after) these pointer data members have been initialized, memory completed the object is not considered properly constructed and eventually the
leaks can still be avoided, though. This is accomplished by using em(smart object's destructor won't be called.
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.
) )
Destructors have problems of their own when they generate The catch clause of a constructor's function tt(try) block behaves
exceptions. Exceptions leaving destructors may of course produce memory leaks, slightly different than a catch clause of an ordinary function tt(try)
as not all allocated memory may already have been deleted when the exception block. An exception reaching a constructor's function tt(try) block may be
is generated. Other forms of incomplete handling may be encountered. For transformed into another exception (which is thrown from the catch clause) but
example, a database class may store modifications of its database in memory, if no exception is explicitly thrown from the catch clause the exception
leaving the update of file containing the database file to its destructor. If originally reaching the catch clause is always rethrown. Consequently, there's
the destructor generates an exception before the file has been updated, then no way to confine an exception thrown from a base class constructor or from a
there will be no update. But another, far more subtle, consequence of member initializer to the constructor: such an exception will em(always)
exceptions leaving destructors exist. 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 Consequently, if incompletely constructed objects throw exceptions then
building a cupboard containing a single drawer. The cupboard is finished, and the constructor's catch clause is responsible for preventing memory
a customer, buying the cupboard, finds that the cupboard can be used as (generally: resource) leaks. There are several ways to realize this:
expected. Satisfied with the cupboard, the customer asks the carpenter to itemization(
build another cupboard, this time containing em(two) drawers. When the second it() When multiple inheritance is used: if initial base classes have
cupboard is finished, the customer takes it home and is utterly amazed when properly been constructed and a later base class throws, then the initial base
the second cupboard completely collapses immediately after its first use. 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) verbinsert(MAIN)(exceptions/examples/destructor.cc)
When this program is run it produces the following output: When this program is run it produces the following output:
verb( verb(
@ -101,10 +158,11 @@ the second cupboard completely collapses immediately after its first use.
Drawer 1 used Drawer 1 used
Abort Abort
) )
The final tt(Abort) indicating that the program has aborted, instead of The final tt(Abort) indicates that the program has aborted instead of
displaying a message like tt(Cupboard2 behaves as expected). Now let's have a displaying a message like tt(Cupboard2 behaves as expected).
look at the three classes involved. The class tt(Drawer) has no particular
characteristics, except that its destructor throws an exception: 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) verbinsert(DRAWER)(exceptions/examples/destructor.cc)
The class tt(Cupboard1) has no special characteristics at all. It merely The class tt(Cupboard1) has no special characteristics at all. It merely
has a single composed tt(Drawer) object: 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 When tt(Cupboard1)'s destructor is called, tt(Drawer)'s destructor is
eventually called to destroy its composed object. This destructor throws an eventually called to destroy its composed object. This destructor throws an
exception, which is caught beyond the program's first tt(try) block. This exception, which is caught beyond the program's first tt(try) block. This
behavior is completely as expected. However, a problem occurs when behavior is completely as expected.
tt(Cupboard2)'s destructor is called. Of its two composed objects, the
destructor of the second tt(Drawer) is called first. This destructor throws Now a problem occurs when tt(Cupboard2)'s destructor is called. Of its two
an exception, which ought to be caught beyond the program's second tt(try) composed objects, the second tt(Drawer)'s destructor is called first.
block. However, although the flow of control by then has left the context of This destructor throws an exception, which ought to be caught beyond the
tt(Cupboard2)'s destructor, that object hasn't completely been destroyed yet program's second tt(try) block. However, although the flow of control by then
as the destructor of its other (left) tt(Drawer) still has to be has left the context of tt(Cupboard2)'s destructor, that object hasn't
called. Normally that would not be a big problem: once the exception leaving completely been destroyed yet as the destructor of its other (left) tt(Drawer)
tt(Cupboard2)'s destructor is thrown, any remaining actions would simply be still has to be called.
ignored, albeit that (as both drawers are properly constructed objects)
tt(left)'s destructor would still be called. So this happens here Normally that would not be a big problem: once an exception is thrown from
too. However, tt(left)'s destructor em(also) throws an exception. Since we've tt(Cupboard2)'s destructor any remaining actions would simply be ignored,
already left the context of the second tt(try) block, the programmed albeit that (as both drawers are properly constructed objects) tt(left)'s
flow control is completely mixed up, and the program has no other option but destructor would still have to be called.
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 This happens here too and tt(left)'s destructor em(also) needs to throw an
though the cupboard having one drawer behaves perfectly. 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 The program aborts since there are multiple composed objects whose
destructors throw exceptions leaving the destructors. In this situation one of 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 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 The bf(C++) standard therefore understandably stipulates that exceptions
em(never) leave destructors. In the cupboard example, tt(Drawer)'s destructor may em(never) leave destructors. Here is the skeleton of a destructor whose
throws an exception leaving the destructor. This should not happen: the hi(destructor: and exceptions)hi(exception: and destructors) code might throw
exception should be caught by tt(Drawer)'s destructor itself. Exceptions exceptions. No function tt(try) block but all the destructor's actions are
should never be thrown out of destructors, as we might not be able to catch, encapsulated in a tt(try) block nested under the destructor's body.
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:
verb( verb(
Class::~Class() Class::~Class()
{ {
@ -160,3 +217,6 @@ a destructor whose code might throw exceptions:
{} {}
} }
) )