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
@ -26,70 +26,127 @@ block. Any exception that may be generated is subsequently caught:
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 counter measures 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
{ {
Composed d_composed;
public:
Incomplete2()
try
:
d_composed(/* arguments */)
{
// body
}
catch (...)
{}
};
)
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.
)
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.
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;
public:
Incomplete2(size_t nChars, size_t nInts)
try
:
d_composed(/* arguments */) // may throw
d_cp(0),
d_ip(0)
{
preamble(); // may throw
try try
{ {
d_memory = new Type; d_cp = new char[nChars]; // may throw
code_maybe_throwing_exceptions(); d_ip = new int[nChars]; // may throw
postamble(); // may throw
} }
catch(...) catch(...)
{ {
delete d_memory; delete[] d_cp; // clean up
delete[] d_ip;
throw; throw;
} }
// onlly nohrow operations here
}
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.
) )
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 situation we're about to discuss may be compared to a carpenter According to the bf(C++) standard exceptions thrown by destructors may not
building a cupboard containing a single drawer. The cupboard is finished, and leave their bodies. Consequently a destructor cannot sensibly be provided with
a customer, buying the cupboard, finds that the cupboard can be used as a function tt(try) block as exceptions caught by a function tt(try) block's
expected. Satisfied with the cupboard, the customer asks the carpenter to catch clause has already left the destructor's body (and will be retrown as
build another cupboard, this time containing em(two) drawers. When the second with exceptions reaching a constructor's function tt(try) block's catch
cupboard is finished, the customer takes it home and is utterly amazed when clause).
the second cupboard completely collapses immediately after its first use.
Weird story? Consider the following program: 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:
{} {}
} }
) )