Passing on exceptions
There must be something wrong with me, since I just don't "get" exceptions, particularly in the C++ world. If you dig around, you can find bunches of web sites which proclaim how they are better at handling certain situations than other approaches. I appreciate that they are describing something which seems meaningful to them, but at the same time, I have yet to see this sort of thing apply to my own projects.
In response to a reader question about exceptions and when they might be useful, I went looking for some C++ code on my system which might actually use them. I actually went into one of the directories where I build things from source which have been downloaded from other places: free software, open source, whatever you want to call it. I specifically avoided looking at my own code, since (surprise) I don't use exceptions.
One thing I surveyed was GNU Radio. It uses SWIG heavily to provide Python support for the library functions which are implemented in C++. This leads to a large number of exception-related hits in the tree, but none of them are really valid for the purposes of this analysis, so I skipped them. I wound up finding something like this:
try { foo = new foo_bar(a, b, c, d); return foo; } catch (...) { delete foo; return 0; }
I had a mixed reaction to this. On the one hand, I had found an actual user of exceptions! On the other, what was going on here? Catching everything that can be thrown by ... a new? Or a constructor? Really? Why would they do that?
I looked at the class it was creating. In there, the constructor's code answered my question. They were doing actual work in the constructor for that class. It initialized variables, sure, but it also initialized hardware and called out to things which could fail. I'm not talking about the "everything can fail" type of failures, but rather "it might not be plugged in" situation which is far more common.
It made sense now. Since they did work in the constructor, and it could fail, they needed some way to convey failures. Constructors don't have a way to return things like an ordinary function, so the only thing left was an exception.
What I don't understand is why they had to rig it this way. My own preference is to have constructors which do little more than initializing values given arguments, like this:
MyClass::MyClass(int a, string b) : a_(a), b_(b) { }
That's it. I take the values I was given (since I'll have them at no other point), sock them away in local storage, and return. I might also create other instances of classes, like this:
MyClass::MyClass(int a, string b) : a_(a), b_(b), mysql_(new MySQLClient) { }
The careful observer will note that this MySQLClient class might do something nontrivial in its constructor. However, it does not. I wrote it, and it also follows my same principle of only doing simple assignment work in there.
Since I control all of the code, I can maintain this restriction. I wish I had a way to make the compiler enforce this sort of policy for me, but it doesn't seem to be possible just yet. In the absence of such tools, it falls to things like good code reviews performed by people who know about this rule and check for it as they work.
Given that I don't do expensive work in my constructor, this means I have to do it somewhere else. My preference is to have an "Init" function within the class. If there is any way for it to fail, then it returns a bool. This gives something like this:
bool MyClass::Init() { if (!mysql_->Init()) { // log something about "mysqlclient failed to init" return false; } // ... other things like the above ... return true; }
Granted, this function doesn't do much. It's mostly responsible for making sure all of the other classes that might be used are ready to go. My MySQLClient class Init function is far more interesting. This is an approximation of what it looks like (with a few parts missing):
bool MySQLClient::Init() { mysql_ = mysql_init(NULL); if (mysql_ == NULL) { // log something about failure in mysql_init return false; } if (mysql_real_connect(mysql_, ...) == NULL) { // log something about failure to connect with result of mysql_error() // clean up mysql_ return false; } return true; }
At this level, I'm at the mercy of someone else's code:
MySQL's Sun's Oracle's libmysqlclient. It can fail in two
different places, and so I test for them, say something if it does, and
report back.
So now let's go up a level and show what might use this MyClass code. It's a simple CLI wrapper.
int main() { MyClass myc; if (!myc.Init()) { // log something about myclass init failing exit(EXIT_FAILURE); } if (!myc.DoStuff()) { // log something about this not working exit(EXIT_FAILURE); } return 0; }
Let's imagine a scenario where this program tries to connect to MySQL and it fails. This is a one-shot CLI tool, so it's appropriate for it to just bomb out with an appropriate exit code. It looks like this:
$ ./cli_wrapper mysqlclient: unable to connect to mysqld (connection refused) myclass: unable to init mysqlclient cli_wrapper: unable to init myclass $
Right there, you have information about the actual failure and enough information to get the call stack straight in your head. If your logging infrastructure is sufficiently clever, it might even include the file name and line number, so it might say "mysql/mysqlclient.cc:123" instead. Hint: __FILE__ and __LINE__ are your friends.
Is there anything my code can do about this failure? No. It can just tell the user and give up. Whether the problem is MySQL not running, the network being down, iptables being screwed up, using "skip-networking" or something else entirely, that's not for this program to solve. Knowing that the problem is "connection refused" in the code would accomplish nothing, in other words.
At this point, now we should see what happens if it's not appropriate to give up just because the database was unavailable. Maybe the outermost process is actually a server, and it needs to stay running. It might not be able to service any requests without that connection, but it needs to keep trying back until it can succeed.
In this situation, you'll get a whole bunch of log spam, since every call to the MySQLClient Init will fail. This is actually what you want, assuming you're not dense enough to call it in a short loop. The moaning from your libraries will let you see why things weren't working when you review your logs later on. It's not supposed to be that way, so spamming the logs is what you would expect. When things are working fine, your warning/error type logs should be relatively quiet.
This is how I build things, big or small. It seems to be working. I can't see any obvious problems with it. Where's the motivation to change?
I'm open to change. If not, I'd still be plinking away at my VIC-20, writing dumb little utilities in BASIC. I just need a good reason.
"Because it's there" is not sufficient for me. Don't try "it's newer". "Everyone else uses it" is not going to cut it, either. "Industry standard" is just a weaselly way of saying the same thing, so that's out, too.
Something more like "it eliminates an entire class of problems" or "there is no other way to accomplish X" would be better. This assumes that the "class of problems" are in fact problems for me, and that "X" is something I want to accomplish. If not, those also fail.
I'm not trying to be difficult. I'm just trying to be productive.