So we will be talking about
You all know about error handling mechanisms in the C++ language right? You know you can throw exceptions, you know you shouldn’t throw them too often, because of performance reasons. You also know you can use error codes (often by enums) to handle the possible error. So is there anything more?
So the reason I would like to talk about the error handling is there is no preferred way to actually handle the error. You cannot simply say, that exceptions are superior to the error codes and vice versa and it’s not only the matter of taste. The C++ community and C++ standardisation group did not simply come up till now with such error handling mechanism, that would suit every case, but most probably that’s going to be changed in the future, but we will come back to this story.
So let’s start the summary of existing practices in error handling domain.
The legacy error codes
Yes the most primitive, yet very powerful technique, used most often till today. Its popularity comes mainly from the ease of use, great performance, and predictability that make error codes suitable for performance critical applications and hard real-time systems.
So let’s show how this looks like. The example of the error code is shown below: test
int sqlite3_open(const char *filename, sqlite3 **ppDb);
We can see, that the function takes the filename as the argument, it “returns” the pointer to the database through its
[[nodiscard]] sqlite_open_errc sqlite_open(/*...*/);
Just to clarify for those of you who did not yet have the opportunity of using C++17, the [[nodiscard]] attribute kindly asks compiler to show the warning to the user if the function is used in discarded-value expression if this isn’t cast to void. So in other words:
sqlite_open("filename", &database_ptr); // shows the warning (void)sqlite_open("filename", &database_ptr); // shouldn't show the warning auto errc = sqlite_open("filename", &database_ptr); // shouldn't show warning neither
But that’s just off-topic.
Coming back to the error codes. Right now what we will most probably do is switch case on the value like that:
switch(errc){ case sqlite_open_errc::file_not_found: // ... case // ...};
That kind of error handling has some pros and cons:
PROS:
Compiler might issue a warning, once not all error codes are handled,
As mentioned it’s very fast, since returning error happens through the stack.
CONS:
business logic is cluttered with error handling logic, which inhibits reading.
In case of errors which are not handled. The error will not propagate to the caller.
Usually some kind of enum conversion is needed to translate error from sqlite domain to the caller domain.
The std::error_code
But that’s not all. There is another way to handle the errors from the functions using the std::error_code class.
We will show how the std::error_code class will help us solve some of the problems and we will see how it’s not perfect in other cases. And the examples will be based on another function from the standard library: std::filesystem::copy, which prototype looks like:
void copy( const std::filesystem::path& from, const std::filesystem::path& to, std::error_code& ec );
The another offtopic here. If you didn’t believe me the error handling is actually a real issue in the C++ world then look carefully at the above function prototype. Did you spot that freaky thing? No? Well, look carefully, if you want to be reported about the errors through the std::error_code, why the function is not noexcept? Actually, it’s not a mistake, have a look at https://en.cppreference.com/w/cpp/filesystem/copy it really is noexcept(false) and this is why: the errors connected with handling the files and its content will be reported by the std::error_code, but it turns out, that the function might need to allocate the memory dynamically and failure doing so will not be reported by the error code parameter but by the exception. That kind of inconsistency really hurts!
Ok so coming back to the topic: How does it actually work? So the whole error handling mechanism is divided into three classes:
- std::error_code contains actual integer representing the error
- std::error_condition can group different error codes from different domains
- std::error_category knows about the domain of error, is able to print meaningful message regarding error code.
For now, I am not going to dig into details on how to actually define own error code nor how does this work under the hood. Actually, I will just point you to a cool blog of Andrzej Krzeminski.
Let’s actually see how we can use already existing error codes in our programs. First thing is, that we can find lots of predefined error codes in the standard library. The predefined set of error codes is enough for replacing throwing exceptions with those values. So let’s have a look at how we can use that:
#include <iostream> #include <filesystem> #include <system_error> int main() { std::error_code errc; auto size = std::filesystem::file_size("file.txt", errc); if(not errc){ std::cout << "everything is fine, file size is: " << size << std::endl; } else if( errc == std::errc::no_such_file_or_directory){ std::cout << "Could not report file size, since file does not exists" << std::endl; } return 0; }
A side note: you might need to explicitly link to the stdc++fs library, so that linking process is successful.
So the first thing we can see is that the std::error_code is castable to bool. Error code casts to true if the error code is different than zero (by default it means, there actually occurred some kind of error).
We can also compare the errors to the predefined error condition constants (here enum class errc ). But this comparison will not work with the switch statement. Because this will not work with the switch and the comparison operator is custom, then the compiler is not able to determine correctly whether all error cases were handled, thus it is not able to hint us we missed some error condition.
At the same time we do not need to handle all error conditions if it’s not in our interest. If we only want to print the error then following snippet will do just fine:
if(not errc){ std::cout << "everything is fine, file size is: " << size << std::endl; } else { std::cout << errc.message() << std::endl; }
The following code’s output is implementation defined, but most likely it will print out something like:
No such file or directory
In case of missing file.
The cool thing about error codes is the way we can use and reuse them with new error_conditions. This you can again see on the Andrzej’s blog.
So to sum things up, std::error_codes:
PROS:
Are more flexible than old style error codes
Once defined are easy to use and provide as much information as exceptions
Blazing fast
Can be displayed in the same way as exceptions with the message member function.
CONS:
Massive amount of boilerplate code needed to define custom error codes and error conditions
Still clutter business logic a little bit.
Exceptions
So we end up analyzing the exceptions. Every C++ developer knows about exceptions and also knows one big thing about them: They are slow.
And of course you can say it’s XXI century, the program’s speed does not matter that much, since we have got good hardware, lot’s of memory so let’s have fun, but unfortunately if you choose C++, then most probably you need to be performance aware (automotive, embedded, HPC) as well, so you need to consider the performance of your program.
So why are exceptions so slow at the first place. This is directly connected to the implementation of the exceptions. They need some additional space either on the stack or someplace else so that the stack unwinding and looking for the catch clauses are possible and this is slow.
In fact, there are so many performance issues with the exceptions, that often in automotive, embedded or any hard real-time systems they need to be disabled. The exceptions are one of those language features that break the “you don’t pay for what you don’t use” rule. It starts to be a little dramatic if we realize that those projects, in fact, use some kind of C++ dialect and not real standard-defined C++.
But there are also good things about exceptions. They are easy to define since anything can be thrown. It’s easy to use existing standard library exceptions as well. They also do not clutter the business logic since the
So to sum up:
PROS:
Automatic error propagation
Business logic separated from error paths
Easy to define custom exception classes
CONS:
Slow (usually only when thrown)
Not predictable in time and space
Future of error handling
People in the committee are trying to solve the issues of the usual exceptions. There already are lots of proposals, that can help us handle the errors those can be the outcome library or std::expected proposal.
First one is macro based features that can handle returning from function union of the error code and normal object, macros could also handle the automatic propagation of the error to the caller etc. The library can be found here. Also, the library should soon be available in boost.
Another proposition to handle the errors is the std:
The main idea of std::expected is to introduce some helper template, that would hold the union of the std::error_code and an “expected” type. The user could just then check whether the error or expected object has been returned and act accordingly.
But the most interesting idea came up in the proposal of Herb Sutter. The proposal proposes to extend the language, so that something like static exception could come to life. So the function after the proposal could be:
- noexcept function not throwing any kind of exception
- function throwing
only dynamic exception - function throwing only static exception
Since the two first points are casual we will focus only on function that can throw static exception
So what this static exception mean? The semantics of that name is that size of the exception is known at compile time. This size is the size of the std::error_code. Such a function could be declared as follows:
int divide(int a, int b) throws;
The throws exception specification indicates, that the function might throw some type similar to std::error_code and at the same time is not allowed to throw any dynamic exception. In case any dynamic exception would be thrown then it’s automatically converted to some kind of
Since the function throws and the size of the exception is known at compile time + the error is small, then we are catching it by value (different than with dynamic exceptions). The possible syntax for that could be:
try { //example from proposal foo(2, 0); } catch(std::error err) { // catch by value is fine std::cout << โ failed, error is: โ << err.error(); }
Another advantage of this approach is, that now compiler sees, that function is annotated with throws, which could issue a warning or compilation error if the error is not actually caught. It was not possible with dynamic exceptions since functions could not be annotated and still could throw the exception.
If we were sure, that function won’t throw we could suppress the eventual warning or compilation error with the following function call syntax:
try foo(2, 1);
So that’s all fine, but how would that work under the hood? So once the compiler sees the function that would be annotated with throws it would automatically change the return type of the function from T to something similar to std:
This approach could even make the feature compatible with the C language so that the static exceptions could be thrown in C++ and caught in C and vice versa. The proposed change to the C language would be to introduce the _Either macro so the function could look following:
_Either(int, std_error)do_something(double); //taken from the proposal
So, in the end, we could end up with the error mechanism with the following characteristics:
PROS:
As fast as old style error codes
Automatic error propagation
Error paths distinguished by catch clause
Predictable in time and space
CONS:
Yet another error handling solution
Summary
As we can see the error handling in C++ isn’t currently in