Raise exceptions my way
- By Giuseppe Puoti
- lun 14 novembre 2016
As a programmer I want to be proud of what I write and I'm proud of those rare peace of code that I've written. In particular my aim is to be able to write a code that people can understand without dealing with all the details. A code which you can reason with on a logical layer, able to be also explained to non-programmers. A Code that seams not to be affected by those strange unexpected border cases that you need to deal with ugly special cases. Writing such a code is not really simple and it may become even more difficult once you variables like requirements and strict timing bring into the equation.
To me as a reader, the most annoyng part of a code, the one that distract me the most, is error checking; I really hate block nesting and, error checking, is one of those thing that inject that kind of "unnecessary" complexity into our code. Exceptions help really much that way.
I've found more than one person that oppose their use, arguing that they act as additional exit points or that they slow down your code. I don't want to write about the execution speed drop because Modern C++ implementations reduce the overhead of using exceptions to a few percent (say, 3%) and that's compared to no error handling [1] , my real concern is write clear self documenting code.
Say you have to check the parameters you receive into your function. In a simplicistic case you might have:
int divide( int x, int y, int& r){
bool error_code = 0;
if (y != 0){
r = x/r;
}
else{
error_code = 1;
}
return error_code;
}
int main(){
int r;
int x = 43;
int y = 3;
if(divide(x, y, r) == 0){
std::cout << x << " / " << y << " = " << r;
}
else{
std::cout << "only Chuck Norris can divide by zero!"
}
}
This code block may become something like the following if you go for exceptions:
struct parameter_out_of_range{
parameter_out_of_range(int x)
: std::exception("unpermitted divider value")
{ }
};
int divide(int x, int y){
if(y == 0)
throw parameter_out_of_range(y);
return x/y;
}
int main(){
int x = 43;
int y = 3;
try{
int r = divide (x,y);
std::cout << x << " / " << y << " = " << r;
}
catch(std::parameter_out_of_range e){
std::cout << e.what();
}
}
with the second version even with this simple function you have:
- better interface
- in the first version the integer division is not a binary function as one might expect
- less complexity
- you do not have to deal with the error_code value into the code of the function
- less noise
- better separation between functional code and error handling code
A nicer technique
I'm writing this article because I think there is some way to have even clearer code with a bit of syntax sugar. What I'd like to have is a way to declare my function is going to throw some kind of exception in some particular situation. This may appear not necessary in the simplicistic situation depicted as in the example, but think about of how much more checking you would be asked with your production code.
bool is_zero(int x){
return x == 0;
}
struct parameter_out_of_range{
parameter_out_of_range(int x)
: std::exception("unpermitted divider value")
{ }
};
int divide(int x, int y){
raise <parameter_out_of_range>(y).when(is_zero(y));
return x/y;
}
int main(){
int x = 43;
int y = 3;
try{
int r = divide (x,y);
std::cout << x << " / " << y << " = " << r;
}
catch(std::exception e){
std::cout << e.what();
}
}
This is a pretty more self documented function reading only the code you can easily understand which is the beaviour of the function in case errors occur. It will:
raise a <parameter_out_of_range> exception on y when y is zero
The cost
Unfortunatly we don't achieve that expressiveness for free. My implementation of helpers that permit that code, requires the exception to be always constructed and eventually raised. This is an unaffordable cost, it is roughly 100x slower than the plan exception raise solution. Ok, so let's reorder things a bit to obtain this kind of code:
int divide(int x, int y){
raise <parameter_out_of_range>()
.when(is_zero(y))
.on(y);
return x/y;
}
This has basically the same cost as the plain exception handling and has most of the benefits of the raise's frist implementation. It clarify that the divide function will:
- raise the exception parameter_out_of_range
- when y is equal to zero
- and it will raise the exception on y to give a better advice on what the problem is about.
The implementation
raise is a simple struct whose only job is bind the type of exception we will eventually raise (and construct). The when method of raise is a factory method to construct an internal object that bind even the condition result and export the on method whitch finally is the one that actually construct the exception and throw. It is pretty simple, here is the complete code:
template <typename ExceptionT>
struct raise {
struct _when {
bool _raise;
_when(bool failing_predicate)
: _raise(failing_predicate)
{ }
template <typename... Ts>
void on(Ts... args) {
if (_raise)
throw ExceptionT(args...);
}
};
_when when(bool failing_predicate) {
return _when(failing_predicate);
}
};
Using variadic template it is possible to have this simple implementation and yet be able to construct any type of exception with any number of parameters.
[1] | from https://isocpp.org/wiki/faq/exceptions |