When should I use std::expected and when should I use exceptions? Take this function for example:
int parse_int(std::string_view str) {
if (str.empty()) {
throw std::invalid_argument("string must not be empty");
}
/* ... */
if (/* result too large */) {
throw std::out_of_range("value exceeds maximum for int");
}
return result;
}
I want to distinguish between different errors when using this function, so it's useful that I can throw different types of exceptions. However, I could also do that with std::expected:
enum class parse_error {
empty_string,
invalid_format,
out_of_range
};
std::expected<int, parse_error> parse_int(std::string_view str) noexcept {
if (str.empty()) {
return std::unexpected(parse_error::empty_string);
}
/* ... */
if (/* result too large */) {
return std::unexpected(parse_error::out_of_range);
}
return result;
}
Are there any reasons to use std::expected over exceptions (performance, code size, compile speed, ABI), or is it just stylistic preference?
First of all, whatever error-handling strategy you are planning to use - establish it at the very beginning of the given project - see E.1: Develop an error-handling strategy early in a design. Because the idea of changing this strategy "later" will most probably result in having 2 strategies: the old one and the new one.
Sometimes, the choice is easy: when, for whatever reasons, exceptions are not allowed in the given project, just use std::expected.
It is really hard (I'd say, impossible) to propose one error handling strategy, that fits all needs. I can only put here just one recommendation, that I try to follow:
The one of possible error-handling strategies, that can be called follow the names:
exceptions for exceptional, rare, unexpected cases. When possibility that the throw-instruction is really called is low.std::expected for errors that are expected.Sometimes it might mean that both ways are used in a single function - like the function returns std::expected<T, E> for Error that is expected, but the function is not marked as noexcept because it can throw in some very rare cases. But if your established error-strategy is that functions returning std::expected<T,E> will never throw - then you need to have this "unexpected" errors be a variant of E.
When applying this strategy to the question case, then std::expected should be selected, unless the input string is already validated according to your design - so, then the errors in parsing are not expected - so: exceptions. But most probably errors will be not totally unexpected - so std::expected. If the function can be noexcept or noexcept(false) - then this is really something that depends on its implementation:
std::expected<int, parse_error> parse_int(std::string_view str) noexcept {
if (str.empty()) {
return std::unexpected(parse_error::empty_string);
}
/* ... */ // Here, if exceptions can happen, but are rare - you should not add `noexcept` to this function signature
if (/* result too large */) {
return std::unexpected(parse_error::out_of_range);
}
return result;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With