Thursday, August 6, 2009

Changing Return Values to Exceptions, C++ Edition

In my last post, I discussed one reason why exceptions are superior to return values from the perspective of an API client. Despite this, you might still run into APIs (albeit hopefully legacy ones) that heavily use status codes and magic return values to indicate errors, and it may not be within your sphere of influence to change. A friend of mine asked me how to deal with this situation.

Ideally we would like to map error results to exceptions. One approach that works in C++ is to leverage the preprocessor via a "failure-checking" macro.

Consider my example from the last post which uses a couple of simple functions to calculate the circumference of a circle given the area. Here is the original client code:
float area = TEST_AREA;
float radius = radiusFromArea(area);
float circumference = circumferenceFromRadius(radius);
cout << circumference << endl;
Let's say that "radiusFromArea" uses the original version of the "mysqrt" function which returns -1 when it receives invalid input, and let's also stipulate that we cannot change the API for any of these functions. We'd like to present an exception to our callers as soon as we detect a bad value returned from one of the functions. We could do that with some spaghetti-ish code:
float area = TEST_AREA;
float radius = radiusFromArea(area);
if( radius < 0 )
{
  throw runtime_error("cannot be negative");
}
float circumference = circumferenceFromRadius(radius);
if( circumference < 0 )
{
  throw runtime_error("cannot be negative");
}
cout << circumference << endl;
(Ideally we would get the exception from "mysqrt" like I demonstrated last time, but recall that for the purposes of this post, we're pretending that we cannot change that function. And yes, in this contrived situation, we could easily check the negativeness of "area", but that would defeat the instructional value of the example.)

As far as isolating where a negative value might be coming from, that works. But it's annoying and ugly and it takes too much typing. That's why C/C++ programmers traditionally tend to be pretty bad about this type of error-checking. We can clean it up considerably with a macro:
#define IfNegativeThrow(x) { \
    if(x < 0 ) \
      throw runtime_error("cannot be negative"); \
    }
The new client code becomes:
float area = TEST_AREA;
float radius;
IfNegativeThrow(radius = radiusFromArea(area));
float circumference;
IfNegativeThrow(circumference = circumferenceFromRadius(radius));
cout << circumference << endl;
This is functionally equivalent to the immediately-preceding example, but it is more concise and (in my opinion) cleaner. You can potentially improve on it by adding an extra macro parameter for the exception message. Macros similar to IfNegativeThrow() can be found in many industrial-strength C++ codebases. This pattern is especially prevalent in old-school COM programming, where it is often used to deal with the infamous HRESULT.

(Shout-out to anybody who still does COM programming like that on a regular basis. Last time I did, I think Bill Clinton was still President.)

There remain some significant drawbacks here. Doing those assignments inside macros still leaves you with less-than-aesthetic code. You still have to maintain discipline about using the macros. And if you are in a language that doesn't support the preprocessor, things get more difficult and/or annoying. (You may have to implement IfNegativeThrow() as a full-fledged method, for example, and incur the requisite overhead.) In a later post, I'll address these issues.

2 comments:

Jeff said...

Hi, How are you enjoying
How to think about algorithms?

http://www.cse.yorku.ca/~jeff/notes/3101/

all the best Jeff

Scott McMaster said...

Hi Jeff. Thanks for checking in. I have been working through the book a bit at a time (as I have several other books in progress ;-)). I am always looking for a fresh take on "classic" subjects, and this is a great example. Consequently, I have recommended it to several people. And I had been remiss in not mentioning it in a separate blog post, which I just remedied. Take care.