When to use std::expected instead of exceptions
Asked Answered
D

1

13

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?

Diverticulum answered 12/6, 2023 at 22:16 Comment(4)
IMHO it's a dupe of #4671487 and #1849990. std::expected does not change those approaches.Bankroll
@273K some of the issues with status codes in the top answer don't apply to std::expected. 1. you have syntax sugar thanks to std::expected being convertible to bool, so your code is still clean; 2. You use the return type, you don't return just a status code. 3. You can carry as much information as you want; 4. still applies. IMO error handling with std::expected is distinct from simply return a status code.Diverticulum
std::expected is just a struct. You are right, it's just a sugar. Nothing new can be added to the linked answers "when to use".Bankroll
Now that this question is closed, performance is reliably better with std::expected. Binary size is likely smaller with std::expected. Your ability to respond to every possible error only exists with std::expected.Miss
T
1

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:

  1. Use exceptions for exceptional, rare, unexpected cases. When possibility that the throw-instruction is really called is low.
  2. Use 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::excpected<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;
}
Terriss answered 23/4 at 7:5 Comment(4)
So in fact what you're suggesting is to adopt 2 strategies from the outset.Gagnon
@Gagnon single strategy can have several points. In reality it is inconvienient to implement std::expected<..> every_function(...) noexcept strategy - because external libs (including STL) do not follow this strategy - they can throw - so you end up with try-catch every such places. Allowing to throw - but only to throw something really almost impossible to happen is not so bad. On the other hard - use only exceptions - even for errors that really happened - make program too slow - exceptions are costly when thrown/caught.Terriss
Also worth noting that while the second strategy is "Use std::expected for errors that are expected", the 'expected' in the class name refers to the non-error value, not the error. Errors are reported using "return std::unexpected(parse_error::out_of_range)". i.e. Report an 'expected error' using a class called... erm... 'unexpected'. I'm not saying this dual-strategy (or if you prefer, two-part strategy) isn't valid & useful, just that your framing in terms of 'expected' & 'unexpected' is at odds with the way std::expected & std::unexpected actually use those terms.Gagnon
+1 @Jeremy, good point. But, well, I can also say it is unexpected that is a part of expected result object, i.e. unexpected which is expected. Anyway - these are just words. -Terriss

© 2022 - 2024 — McMap. All rights reserved.