mirror of
https://gitlab.com/fbb-git/cppannotations
synced 2024-11-16 07:48:44 +01:00
Exceptions completed
git-svn-id: https://cppannotations.svn.sourceforge.net/svnroot/cppannotations/trunk@311 f6dd340e-d3f9-0310-b409-bdd246841980
This commit is contained in:
parent
e84e0bb7b9
commit
525d470554
3 changed files with 160 additions and 104 deletions
|
@ -72,7 +72,6 @@ includefile(classes)
|
|||
lchapter(MEMORY)(Classes And Memory Allocation)
|
||||
includefile(memory)
|
||||
|
||||
COMMENT(>>>>>>>>>>>>> NEXT <<<<<<<<<<<<<)
|
||||
COMMENT( 9 )
|
||||
lchapter(EXCEPTIONS)(Exceptions)
|
||||
includefile(exceptions)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
|||
{}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue