Error handling is an essential part of software development, and the C++ has evolved over time to provide various mechanisms for handling errors and exceptions. In this blog post, we will explore the history of error handling in C++ and how new std::optional
and std::expected
classes are useful for it.
Error Handling in C++
One of the earliest approaches, which was used in the C programming language, was the use of a global error variable called errno
to indicate the success or failure of a function. This approach has several drawbacks, including the potential for errno
to be overwritten by another function, and the difficulty of providing detailed information about the error. Therefore, using errno
is generally not considered a good idea in C++.
Another traditional approach to error handling is to use the return
statement to indicate the success or failure of a function. Functions would typically return a special sentinel value (such as -1
or nullptr
) to indicate an error, and the caller would be responsible for checking the return value and handling the error appropriately. One of the drawbacks of using the return
statement to indicate the success or failure of a function is that it can be easy for errors to be overlooked or mishandled. This is because the caller of a function must check the return value and interpret the meaning of the error code, which can be difficult to do consistently across a large codebase. In addition, using sentinel values such as -1 to indicate an error can be difficult when the function is expected to return a range of valid values that includes error codes.
Another approach that requires mentioning is exceptions. But exceptions also have their own drawbacks, including the overhead of creating and propagating the exception object, and the potential for unexpected behavior if the exception is not caught. This can be a concern for performance-critical applications. In addition, exceptions can be difficult to debug, because the point where the exception is thrown may not be the same as the point where it is caught.
One of the common drawbacks of the traditional error-handling techniques in C++, such as returning error codes or using exceptions, is that they do not explicitly indicate which functions might throw or return an error. This means that it is the responsibility of the user of the function to be aware of the possible errors and handle them appropriately, which can be difficult and error-prone.
Optional and Expected
std::optional
and std::expected
are two new classes introduced in C++17 and C++23 respectively and they provide a more expressive way of handling errors and exceptional conditions. These classes can be used as alternatives to returning error codes or using exceptions, and provide a safer and more intuitive way of representing and handling errors in C++.
std::optional
wraps a variable and its existence information, so that it can represent the possibility that a function may not return a value. std::expected
is similar but it allows you to return an error object from a function. For example, a function that returns an std::expected<int, std::string>
can return either a normal int
value, or an error object of type std::string
, which can be used to provide detailed information about the error.
In the example below, the generatePost
function takes a title as input, and returns a std::optional<BlogPost>
. If the title is empty, then the function returns an empty std::optional
, using the {}
initializer or the std::nullopt
value. Otherwise, the function magically creates a post and returns it as std::optional<BlogPost>
.
std::optional<BlogPost> generatePost(Title title){
if (title.empty()){
return {}; // OR std::nullopt
}
BlogPost post = doAIStuff(title);
return post;
}
Accessing the value of the optional
or excepted
object when it doesn’t hold a value is an undefined behavior. Therefore, it is important to check if the optional contains a value before accessing it, using the has_value()
function or the bool ()
operator. In the example, generatePost
function is used and its result is stored in a std::optional<BlogPost>
object. If it contains a value the post is published, otherwise an error message is printed.
auto myNewBlogPost = generatePost(fancy_title);
if (myNewBlogPost.has_value()){
publish(myNewBlogPost.value);
} else{
std::cout << "Even AI cannot generate a post without a title!\n";
}
In the case where a function does not return a value, it does not necessarily have to be considered an error. The program can continue as usual using a default value. In such cases, the value_or
function can be useful. This function checks if a value exists in the optional object and returns that value. If no value exists, it returns the value that was passed to the function. So in the example, if we have a default blog post, we can use it as follows.
// Instead of
if (myNewBlogPost.has_value()){
publish(myNewBlogPost.value);
} else{
std::cout << "Title is empty! Publishing a Lorem Ipsum Text.\n";
publish(lorem_ipsum_post);
}
// In one line
publish(myNewBlogPost.value_or(lorem_ipsum_post));
Consider the case where the generatePost
function receives an additional input specifying a minimum fitness score for the generated post and does not return posts with a lower score. Using std::expected would be a better option for this scenario because it can provide the user with more information about why a value was not returned, allowing for a more effective handling of the issue.
std::expected<BlogPost, std::string> generatePost(Title title, FitnessScore fitness_limit){
if (title.empty()){
return std::make_unexpected{"Even AI cannot generate a post without a title!"};
}
BlogPostwithFitness result = doAIStuff(title);
if (result.fitness < fitness_limit){
return std::make_unexpected{"Unfortunately our AI is not at that level yet!"};
}
return result.post;
}
auto myNewBlogPost = generatePost(fancy_title);
if (myNewBlogPost.has_value()){
publish(myNewBlogPost.value);
} else{
std::cout << myNewBlogPost.error();
}
Monadic Functions
The upcoming release of C++23 will include monadic function support for std::optional, also there is a proposal for std::expected. These updates will introduce new member functions called transform
, and_then
, and or_else
, which allow for the chaining of optional functions. This can result in cleaner and more concise code.
The transform
function is used to apply a function to change the value (and possibly the type) stored in an optional. It takes a function as input, applies it to the value stored in the optional, and returns the result wrapped in an optional. If there is no stored value, then it returns an empty optional.
The and_then
function is used to compose functions that return std::optional. It takes a function as input, applies it to the value stored in the optional if it is present, and returns the result. If the optional is empty, then the entire expression evaluates to an empty optional.
The or_else
function returns the optional if it has a value, otherwise it calls a given function and returns the result. This allows for the handling of empty optionals in a functional manner.
In the example below, you can see how chaining monadic functions simplify the code with if-else blocks.
/// Without Monadic Functions
auto valid = checkTitle(title);
if (valid){
post = generatePost(title);
} else{
reportFailure();
return;
}
if (post.has_value()){
publish(result);
} else {
reportFailure();
}
/// With Monadic Functions
checkTitle(title)
.and_then(generatePost)
.transform(publish)
.or_else(reportFailure);
Conclusion
In conclusion, std::optional
and std::expected
are useful tools for error handling in Modern C++. std::optional allows for the representation of missing values, while std::expected allows for the inclusion of error information in the returned value. These types can greatly improve the clarity and reliability of C++ code. Although they are available with C++17 and C++23 respectively, you can check these repositories to use optional and expected with the earlier C++ versions.
Finally, if you have both optional and excepted available I would suggest:
Function may or may not return a value and that is expected → Use optional
Function is expected to return a value but if it fails you don’t care the reason or there is a single reason for that → Use optional
Function is expected to return a value but might fail with several reasons, and you have different handling mechanisms for different error cases → Use expected