mirror of
https://gitlab.com/fbb-git/cppannotations
synced 2024-11-18 10:06:54 +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)
|
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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
Composed d_composed;
|
||||||
|
public:
|
||||||
|
Incomplete2()
|
||||||
try
|
try
|
||||||
|
:
|
||||||
|
d_composed(/* arguments */)
|
||||||
{
|
{
|
||||||
d_memory = new Type;
|
// body
|
||||||
code_maybe_throwing_exceptions();
|
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{}
|
||||||
delete d_memory;
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
)
|
)
|
||||||
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:
|
||||||
{}
|
{}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue