James G. Sack (jim) wrote:
Christopher Smith wrote:
Andrew Lentvorski wrote:
..
Unfortunately, constructors and destructors are excruciatingly
difficult to get right in the presence of exceptions. The fact that
that you have to pull out about 3 different books to get this right
says that something is broken.
Or perhaps that there is something that can be learned.... ;-)
Most of the people who can actually explain clearly to me the
differences between destructors and finalization will observe that the
former is far more useful than the latter, and they both make it
difficult to accurately characterize the behaviour of a system
(finalizers manage to do so even without adding exceptions in to the mix
;-).
Can someone explain clearly the differences?
Seriously.
A quick glance at some googlehits and w'pedia doesn't give me a feel for
why this is a significant question, although I gather it's (maybe?) got
something to do with deterministic concerns (perhaps correctness?).
So, the differences are subtle, but as it turns out, significant. A
finalizer is run when an object is determined to be unreachable, whereas
a destructor is run when an object is being destroyed. This is important
because technically an object isn't destroyed either before or after a
finalizer is run. In fact, it is possible for the code in a finalizer
might cause an object to become reachable again, sort of like
"reincarnating the object", except that the object never died in the
first place... As with garbage collection, finalizers usually aren't
guaranteed to run at any particular time or even at all, whereas the
destructor is guaranteed to run at exactly the time it is destroyed.
Also, in multithreaded systems it isn't unusual for finalizers to run in
a dedicated thread/thread pool that is otherwise unrelated to the
objects being finalized.
Destructors are even more useful in C++ because you can have objects
whose lifetime is scoped to a particular block. That means you can have
guarantees that a destructor will get invoked while you drop out of
scope. In languages that have exceptions but who don't have destructors,
you usually see something like a "finally" keyword that allows
programmers to define a block that is guaranteed to execute when you
leave scope. While it accomplishes much the same thing, it doesn't allow
this cleanup work to be encapsulated in the object (you have to write it
out each time). So, for example, in Java a common idiom is:
Connection conn = null;
try {
conn = ....;
/* do some stuff that may cause exceptions */
} finally {
try {
if (null != conn) {
conn.close(); //give it the ol' college try
}
} catch (SomeExceptions e) {
log.error(e, "Error while closing connection);
}
conn = null; //not necessary, but you see people do this anyway
}
You get dizzy with all those blocks? Follow all the program flow? Now
imagine if you had three different resources that needed to be cleaned
up before you got out of dodge (not too uncommon really). Lots of
nesting of blocks. It's awesome (NOT!).
Now, in C++ the idiom is more like this:
{
Connection conn(.....);
/* do some stuff that may cause exceptions */
}
The "Connection" object's destructor cleans up the connection as best it
can when conn drops out of scope. All the logic for how you tear down a
connection is nicely encapsulated in Connection. The one significant
drawback is that if an exception is thrown and the stack starts to
unwind, the program will terminate if conn's destructor gets invoked as
part of the unwind *and* it throws an exception. The general rule for
this in C++ is "don't throw exceptions from a destructor", although it
turns out there are some very rare cases where that is the right thing
to do. Anyway, Connection's destructor will probably look something like
this:
Connection::~Connection() {
try {
close();
} catch (...) {
log.error("Error when closing connection......");
}
}
There are some more fun cases involving exceptions and cleaning up
resources (from my point of view, particularly with finalizers), but you
get the basic idea.
Oh, and one last fun area for both of them: inheritance.
What if your parent class has a cleanup method, and so do you? Well,
with finalizers, you generally have to remember to explicitly invoke the
parent class's finalizer or it gets ignored. There isn't much a parent
class can do to prevent this beyond making the finalize method
non-overrideable ("final" in Java parlance) and then have its finalizer
do whatever cleanup it wants to and then invokes some overridable
"finalize hook" method that subclasses can get involved with. CLOS has
much prettier ways of doing this than most languages. With destructors,
the parent destructors will get invoked after you've finished your work,
and there's not much you can do to stop it beyond crashing. With C++'s
static binding, there's one more fun thing: if you are going to have
someone derive from your class, you almost certainly need to make your
destructor virtual. Here's why. Imagine class A and class B, with B
being a subclass of A:
class A {
public:
~A(); //destructor for A
};
class B : public A {
public:
~B(); //destructor for B
};
{
A* foo = ....; //one should use auto_ptr's, but I don't want to confuse
B* bar = ....; //folks who aren't super familiar with C++
delete (bar);
delete (foo);
}
Now, what's going to happen when you invoke "delete(bar)"? Well, the
compiler see "bar" is pointer to an object of type B, so it is going to
invoke B::~B() on *bar, and then in turn invoke A::~A() on *bar, before
finally cleaning up the memory for bar. That all seems well and good.
What about what happens when you invoke "delete(foo)"? Well, the
compiler sees "foo" is a pointer to an object of type A, so it is going
to invoke A::~A() on *foo, and then it'll clean up the memory. Sound
good? Well, it is.... if foo really is a pointer to an instance of A.
However, it is entirely possible it could be a pointer to an instance of
B. In that case, whatever extra cleanup work that would happen in
B::~B() is never going to happen. This can result in a resource leak
(ick!). The solution is to make A::~A() virtual. Then the calls to both
A::~A() and B::~B() become virtual function calls, which are guaranteed
to catch whatever overrides might be in derived classes, and the expense
of an additional jump instruction (and having to have a vtable for your
objects, and not being able to inline the destructor quite so easily....).
Anyway, in practice, finalizers prove not to be that useful for most
cases, and also a source of much additional complexity (though
thankfully in the common case most of the complexity is in the hands of
whomever has to write the memory manager) and destructors prove to be
quite useful, particularly for managing non-memory related resources,
but are also a source of much additional complexity.
My observation has been that, in general, cleaning up properly is one of
those things that people naturally tend to think of as being trivial and
unimportant, but programmers quickly learn is actually where a lot of
the work and complexity comes from. Even in languages without
destructors, finalizers, or exceptions, it's not like these problems go
away, just that you deal with resource clean up on a case by base basis
(which allows for a lot of the cases to be quite simple), rather than in
a generalized fashion. People who work primarily in languages without
such constructs seem to often think they save themselves a lot of pain
and suffering by just not having language features for resource clean
up, but my observation is they just aren't quite as aware of how much
more painful it is going to be to deal with the issue.
--Chris
--
[email protected]
http://www.kernel-panic.org/cgi-bin/mailman/listinfo/kplug-lpsg