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

Reply via email to