Everyone who has read Scott Meyer’s books knows: it is a good idea to make your code easy to use correctly and difficult to use incorrectly. In this light, we may be able to improve on the ifs we routinely use to check our functions’ preconditions.
The basic idea is to remove all the ifs that check for error conditions at the beginning of your function’s body by providing type parameters that already give sufficiently strong guarantees.
Things to look out for
If you think this sounds like a good idea and you want to try it out, here are some telltale signs when you might be able to remove error checking responsibility from your functions.
Explicit checks for errors
You actually find ifs checking for error conditions:
void paint_rectangle (int x_upper_left, int y_upper_left, int x_bottom_right, int y_bottom_right, color fill) { if(x_upper_left > x_bottom_right){ throw std::invalid_argument{"upper_left must be left of bottom_right"}; } if(y_upper_left > y_bottom_right){ throw std::invalid_argument{"upper_left must be above bottom_right"}; } for(auto x = x_upper_left; x != x_bottom_right; ++x ){ for(auto y = y_upper_left; y != y_bottom_right; ++y ){ draw_pixel(x, y, fill); } } }
Say you have a rectangle class whose invariant guarantees that upper_left and lower_right live up to their names. Then you could change the implementation to:
void paint(rectangle const& area, color fill) { for(auto x = area.upper_left().x; x != area.bottom_right().x; ++x ){ for(auto y = area.upper_left().y; y != area.bottom_right().y; ++y ){ draw_pixel(x, y, fill); } } }
Although the above example is a bit academic and a more sophisticated implementation of paint_rectangle could make sense of all combinations of integers passed to it, there is no good reason to leave this responsibility to paint_rectangle.
Strings as function parameters
Ok, before you gasp. Of course having a std::string as a parameter is not neccessarily bad, especially when your parameter actually represents an arbitrary text. Many times, however, std::string is used as a URL, an email address, or a file path. In these cases, a more specialized type makes sense. Passing an arbitrary string instead of an email address is likely to result in an error which could have been caught by the type system. std::string is kind of a swiss army knife, since it can hold almost any information, but do not let std::string subvert type safety.
The function throws invalid_argument exceptions
In some cases your function might explicitly throw a std::invalid_argument exception. In this case, you should ask yourself if you could introduce a meaningful type to avoid this code path. “No” may be a perfectly valid answer, but you should at least be suspicious and think about it for a moment.
Benefits
I observed the following benefits to the code base when using expressing preconditions with specialized types:
- Functions become more cohesive, since the responsibility of verifying the preconditions is removed from the function body and expressed in its parameter’s type.
- You need less test code. Think about two functions taking an email address. If their parameter type happens to be a std::string, you need to test the behaviour of both functions for invalid email addresses (like “Hello I’m not an email address”). If on the other side you have an email type constructed from a string, you need only to test this constructor once.
- You can sometimes give noexcept guarantees which you otherwise could not. This is in part because you can remove error checks from the function body, but even more often (at least in my experience) because you need a lot less string operations of which most allocate memory and may fail.
- Often your code gets more efficient along the way. When the same instance is used as an argument in several function calls, the precondition does not have to be checked over and over again.
Thanks for reading this, let me hear your thoughts in the comments.
I recently found out that Andzej has also written about precondtions in his blog: http://akrzemi1.wordpress.com/2013/01/04/preconditions-part-i/
Markus,
but should we really throw exception in this case? In my understanding the caller violates precondition and thus it leads to UB.
Hello Alex,
thanks for your feedback. I fear I have to disagree with you. Here is my point of view:
Depending on your requirements (e.g. real time, legacy code base wich is not exception safe, …) it does not alway have to be an exception. That being said, I would strongly discourage introducing undefined behaviour into your library or application, even in the case of an error.
How would we even test, that a functions behaviour is undefined for a set of input variables? Well, we would not. Feels wrong already.
Bugs causing undefined behaviour are usually a lot harder to find and reproduce. Someone might spend a lot of time to find a strange bug caused by two function arguments in the wrong order, instead of developing that new awesome feature. I do not like the thought of punishing users for mistakes and errors, but aim to support them by delivering robust libraries, which behave well in case of errors, and even deliver helpful debug messages.
The only reason to omit a check for preconditions I can think of is performance. Thanks to branch prediction of modern CPUs
if
statements are laughable cheap. Please be careful if performance is your motivation to not assert your functions preconditions.Best regards, Markus