Self document your code and let users decide how to handle error
Wednesday 29 June 2016
In a previous post I demonstrated how handy error handling in Haskell and in Rust is.
It is possible to do the same in C++.
Too often I come along the problem of error handling in C++. Some believe
we must use exceptions
, others prefer the old C style error code.
I believe both are good options and have valid concerns about the other. I believe we should do both.
If you were to open a file, in C you would use the following function:
int open(char const* path, int oflag, ...);
open
returns either -1
or a valid file descriptor. If you have an
error and you want to find out what happened you will need to check errno
then try to guess if EACCES
is the one you are concerned about.
It is not easy to use but it has been working for the last couple of decades.
C++ comes with some wrapper around the C function open
. There is a
class fstream
which provides the method
open.
void open (const char* filename,
ios_base::openmode mode = ios_base::in | ios_base::out);
If the function has failed, you will need to look at the
ios::good to find out if the
function succeeded or otherwise the opened fstream
would throw an exception on
the next operation, fair enough. The best practice is clearly to check for
ios::good
function.
I think we can do much better. Ideally, we would like the signature of the function to be self explanatory, to describe precisely what users of this function should expect.
Boost provides a data structure: boost::variant
. It is an
enhanced version of the C
's unions
, with strong typing.
// you can create an object which is either an `int` or a `std::string`
boost::variant<int, std::string> obj = 10;
// you can get it's content knowing its type
int v = boost::get<int>(obj); // OK
// you can mutate the content
obj = "Prime Type Ltd"; // Ok
// and access its value with its type
std::string p = boost::get<std::string>(obj); // OK
// but you cannot get it wrong:
boost::get<int>(obj); // obj has been set to a std::string...
This is really similar to what I described in my previous post.
We will wrap boost::variant
in a structure and provide Rust-like
method to map the content of the value, chain with other function etc...
template<class result_type, class error_type>
struct Result {
boost::variant<result_type, error_type> variant_;
};
And then we could do the following:
Result<std::fstream, std::runtime_error> open( char const* filename
, ios_base::openmode mode = ios_base::in
| ios_base::out
);
This is it !
We now have a function, which explicitly tells us what the function is doing.
It is a function which open
the file, takes a filename
and mode
, and
returns either the successfully opened fstream
or a runtime_error
.
Result
You can find the complete source code with the test on github.
Now we can do pretty interesting thing with this:
We can throw an exception if we don't want to handle the error right now:
// either open succeed or throw the `std::runtime_error`
auto fs = open("/invalid/file.txt").unwrap();
Or we can recover:
// if we fail to open the file we would execute the given lambda.
auto fs = open("/invalid/file.txt")
.or_else([](std::runtime_error&& )
{
return std::move(open("/valid/file.txt"));
}
);
Or even more complex yet interesting chaining:
std::string line = open("example.txt")
.and_then([](std::fstream&& fs) { return readline(fs); })
.and_then([](std::string&& s) { return parseline(s); })
.or_else([](std::runtime_error&&) {return open("test.txt")})
.and_then([](std::fstream&& fs) { return readline(fs); })
.and_then([](std::string&& s) { return parseline(s); })
.expect("cannot parse line of both example.txt and test.txt");