Throwing an exception from a function is not a good idea if you want to prevent the user from ignoring or forgetting the errors. The exceptions are invisible in the function signature and are therefore very easy to forget and ignore. If the errors don't happen very often, it is easy for code to pass all testing and code review and end up crashing in production. It is also difficult to know which exceptions the function can throw unless the function is exceptionally well documented. It is much better to not allow code that ignores errors to compile.
I don't know of any library that implements a result type for C++, which wouldn't allow code that ignores error paths to compile, but it is possible to use std::variant<T, E> and std::visit to implement a Result -class, which forces you to handle both the success and error cases and doesn't allow access to the result, when an error occurs. The idea is to create a match member function, drawing inspiration from Rust pattern matching, which forces the user to provide the implementations for both execution paths.
The match functions takes lambdas as arguments and uses the overloaded trick to create a handler for std::visit. Then it calls std::visit for the result. Here is a simplified example of how this would work, a proper implementation should consider the details more carefully.
#include <variant>
#include <string_view>
#include <iostream>
template<typename T, typename... Es>
class Result {
template<class... Ts>
struct Overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts>
Overloaded(Ts...) -> Overloaded<Ts...>;
std::variant<T, Es...> result;
public:
Result(decltype(result) value) : result(value) {}
template<typename Type>
Result(Type value) : result(value) {}
template<typename... Lambdas>
[[nodiscard]] auto match(Lambdas... lambdas) {
Overloaded handler{lambdas...};
return visit(handler, result);
}
};
struct Error1 {
std::string_view text;
[[nodiscard]] std::string_view message() { return text; };
};
struct Error2 {
std::string_view text;
[[nodiscard]] std::string_view message() { return text; };
};
[[nodiscard]] Result<int, Error1, Error2> libraryFunction(int val) {
if(val > 0) {
return 0;
} else if(val == 0) {
return Error1{"Error 1"};
} else {
return Error2{"Error 2"};
}
}
int main() {
std::cout <<
libraryFunction(1).match(
[](int res) { return res; },
[](auto err) -> int { std::cout << err.message() << std::endl; abort(); }
)
<< std::endl;
libraryFunction(0).match(
[](int res) { std::cout << res << std::endl; },
[](Error1 err) { std::cout << err.message() << std::endl; },
[](Error2 err) { std::cout << err.message() << std::endl; abort(); }
);
libraryFunction(-1).match(
[](auto res) {
if constexpr(std::is_same<decltype(res), int>())
std::cout << res << std::endl;
else {
std::cout << res.message() << std::endl;
abort();
}
}
);
return 0;
}
The user can still provide an empty error handler, but it is very hard to forget it by accident here. It should also be pretty easy to notice in code review when an error needs to be handled and whether the handler is empty. If you add a new error type to the function, code which cares about the type of the error will also stop compiling until it is fixed. Exceptions are horrible from this point of view and peppering them everywhere in standard libraries makes a lot of code dangerous in embedded systems where exceptions and dynamic memory allocation are not allowed. Basing this on boost::variant2 instead of std::variant might also make sense to get rid of having to consider the valueless by exception -state.
The interfaces provided by std::expected allow the user to not handle the unexpected cases and the operator* interface even allows undefined behavior. It is also missing an equivalent of the match -method, which seems like the safest option. The problem with allowing any extra interfaces for the user, which allow errors to be ignored, is that they are likely to use them instead since most don't like error handling.
The naming of expected and exceptions is also poor for something meant for error handling. Calling errors exceptions or unexpected makes the programmer think errors are exceptional, less common and therefore less important. In many situations errors can actually be more likely and even more important to than the successful execution path.