2012-03-21

Don't call virtual functions in a destructor

Sometimes I like to define the outline of an operation in a base class and then have it delegate to virtual functions in derived classes. This is the Template Method design pattern.

class Base {
  uint Counter;


  void foo() {
    ++Counter;
    _foo();
  }


  pure void _foo();
}


class Derived {
  virtual void _foo() {
    Console.WriteLine("In Derived::_foo");
  }
}



Last night I tried to gin up a sort of "stack guard," something that would let me perform an operation once I got to a certain point in a function, but which also would be executed if the function returned early. When something needs to be done at the end of a block of code, I usually define a new class that has code in its destructor. For example, a lock class could take a critical section and acquire it in its constructor and release it in its destructor, and you just need to create the object in the appropriate place to perform your thread-safe operation. I wanted the same thing, but just a bit more complex, where I could call the release function manually once I'd reached a certain point and then be assured it would not be called in the destructor.

class Committer {
  bool Committed;


  ~Committer() {
    if (!Committed) {
      commit();
    }
  }


  void commit() {
    _commit();
  }


  pure void _commit();
}


class SafeDB: Committer {
  SafeDB(Database db): Committer() {
    // open a database connection
  }


  virtual void _commit() {
    // commit any pending transactions
  }
}



Simple enough. You can quibble about whether it's really worth it to define a class hierarchy for such simple functionality, but I've been trying hard to increase the amount of abstraction I use in EnScript to reduce code bloat and time wasted debugging.

The problem is that this doesn't work and generates run-time errors about null references. Why?

Answer: SafeDB's destructor is called first, and then Committer's destructor is called. As part of executing SafeDB's destructor, it nulls out whatever class members are a part of SafeDB before calling Committer's destructor. Hence, by the time you get to Committer's destructor, you really only have a Committer object, not a SafeDB object. The virtual table is still active, however, so the call to SafeDB::_commit() happens successfully, but it finds itself confronted with no class variables. Boom!

So, this is now another little corner of the EnScript world that's been explored. Time to light out for the territory...

2 comments:

  1. I'm not quite sure how you reach a "don't use virtual functions from a destructor" policy here: that seems like reaching far too general a conclusion from what seems to be a very tractable problem.

    Destructors have a lot of corner cases, yes. But three basic rules will avoid a lot of headaches (including this one).

    1. They should never throw exceptions, ever
    2. They must not rely on virtual functions that are defined below them in the hierarchy
    3. If there exists even one virtual function in the class, the destructor itself needs to be declared virtual.

    ReplyDelete
  2. Hi Rob,

    This post is specific to EnScript, an embedded programming environment in Guidance Software's EnCase. In EnScript, you just _cannot_ call a virtual function from a class's destructor.

    In C++, this wouldn't be a problem. I actually disagree a bit with your advice in #1; there are some instances when throwing from a destructor is not a bad solution, but, of course, the context needs to be known that nothing else could possible throw before such an exception would be caught. For example, in a unit testing context, it can be convenient to use constructors/destructors for setup/teardown semantics, and you can be confident that the unit testing mechanism will catch exceptions thrown from the destructor.

    Jon

    ReplyDelete