I was looking at Doug Ross’s blog the other day; in particular his beef with exception-based programming. I definitely have more C++ experience than Java, so whenever I think about exceptions I think in the context of exception safety, transactional semantics, and RAII. Here’s my take on what he said:

Exceptions tacitly encourage abdication of cleanup responsibility… But the problem is, very few people are disciplined enough to set try, catch and finally clauses for every single method.

I would never expect every single method to have a try/catch/finally block associated with it. In my opinion, that’s a waste of the exception model, and severely limits its advantages. When I layer code, I want to consider sources (in the dark hearts of libraries) and sinks (in the application interface or controller) of exceptions. I try to minimize the exception handling (try/catch/finally) performed anywhere in between.

Wait! How can less exception handling produce better results? In C++ at least, the answer is “stack management”. Local objects (anything not allocated with new) are created on the stack. If an exception occurs, the stack is unwound, calling destructors for all instantiated local objects. If all of my setup code occurs in C++ object constructors, and all of my teardown code occurs in the object destructors, then the compiler will essentially write my try/catch/finally itself. I don’t have to worry about setting flags for what teardown needs to complete; that information is implied by the objects on the stack.

Now, code has transformed from this:

try {
    file = open(myFileName);
    file.readInto(myDataStructure);
finally {
    if (file) file.close();
}

to this:

openFile(myFileName);
openFile.getFileObject().readInto(myDataStructure)

If readInto raises an exception, the destructor of openFile will close the file properly, and I don’t need to worry about it whenever I try to use files. There’s definitely a benefit to be able to avoid this boilerplate checking. The associated cost, then, is creating objects like openFile that act as sentries on my stack.

Sadly, this doesn’t work in Java, because all objects are allocated on the heap, so destructors aren’t reliable. In my opinion, RAII is an idiom that goes hand-in-hand with having exceptions. Becuse C++ classes can put cleanup code in destructors, higher-level application code doesn’t have to perform that cleanup. Lacking that language feature, Java forces application code to perform low-level cleanup. Java’s exception model (particularly checked exceptions) requires developers to add try/catch/finally clauses where they really have no business doing so.

Given the amount of crap the language has received, it may be ironic to hear that PHP actually supports RAII-style programming. As of PHP 5, objects have had destructors which get called automatically when they go out of scope. However, there are 2 caveats:

  • Objects that are created in the outermost scope of the script (e.g. at the top of the page) don’t actually go out of scope until the script exits. I haven’t fully investigated this, but it appears that RAII is only effective within functions.
  • Prior to PHP 5.2, object destruction order was undefined. As of 5.2, object destruction order is the reverse of instantiation order (which is what you would expect from a stack).

Now, here’s something to leave you with. Doug challenged readers to present an exception-based version of a transaction with tunable logging. Here it is, Doug:

#include <string>
#include <iostream>
#include <stdexcept>
#include <time.h>
#include <stdlib.h>

const int DEBUG_LEVEL = 5;

const int OK = 0;

void logit(std::string message, int priority=0){
    if (DEBUG_LEVEL >= priority) {
        std::cerr << message << std::endl;
    }
}

int fake_result() {
    int value = random() % 4 - 2;
    return value < 0 ? 0 : value;
}

template<class T, class Fbool, class Fvoid>
class sentry {
private:
    T &object_;
    Fvoid end_functor_;
public:
    sentry(T &obj, Fbool begin, Fvoid end)
        : object_(obj), end_functor_(end) {
        begin(object_);
    }

    ~sentry() {
        if (std::uncaught_exception())
        end_functor_(object_);
    }
};

template <class T>
class openit {
private:
    std::string name_;
public:
    openit(const char* name) : name_(name) {}
    bool operator() (T &res) {
        if (res.open() != OK) {
            logit("open "+name_+" failed...");
            throw std::runtime_error("o noes");
        }
        logit("open "+name_+" succeeded...", 5);
    }
};
template <class T>
class lockit {
private:
    std::string name_;
public:
    lockit(const char* name) : name_(name) {}
    bool operator() (T &res) {
        if (res.open() != OK) {
            logit("lock "+name_+" failed...");
            throw std::runtime_error("o noes");
        }
        logit("lock "+name_+" succeeded...", 5);
    }
};
template <class T>
class closeit {
public:
    void operator() (T &res) {
        res.close();
    }
};
template <class T>
class unlockit {
public:
    void operator() (T &res) {
        res.unlock();
    }
};

class account {
private:
    int amt_;
public:
    account(int amount) : amt_(amount) {}

    int open() {
        return fake_result();
    }

    int lock() {
        return fake_result();
    }

    void close() {
    }

    void unlock() {
    }

    int adjust(int difference) {
        amt_ += difference;
        return amt_;
    }
};

int transaction(account &credits,
    account &debits, int amount
) {
    credits.adjust(amount);
    debits.adjust(-amount);
    return fake_result();
}

typedef
sentry<account,openit<account>,closeit<account> >
account_opener;
typedef
sentry<account,lockit<account>,unlockit<account> >
account_locker;

int main() {
    srandom(time(NULL));

    account credits(5);
    account debits(3);

    account_opener open_credit(credits,
        openit<account>("credit"),
        closeit<account>());
    account_locker lock_credit(credits,
        lockit<account>("credit"),
        unlockit<account>());
    account_opener open_debit(debits,
        openit<account>("debit"),
        closeit<account>());
    account_locker lock_debit(debits,
        lockit<account>("debit"),
        unlockit<account>());

    if (OK == transaction(credits, debits, 1)) {
        logit("transaction succeeded ...", 3);
    }
    else {
        logit("transaction failed ...");
    }
    return 0;
}
Advertisements