I wanna use std::optional as a Maybe type and I'm concerned wether I can use it to indicate a potential failure in some computation, the so observed as an empty option.
For example, consider:
// This function doesn't have state
std::optional<int> multiply_by_2(std::optional<int> ox) noexcept
{
if (ox)
return {ox.value() * 2};
// empty input case
return {};
}
int main()
{
auto input_handle = get_stdin().lock();
auto output_handle = get_stdout().lock();
// This can panic the application, which is fine at this point
// so just unwrap the optional and call it a day
output_handle.println("Your marvellous calculation: {}", multiply_by_2(input_handle.get_line().as_int()).value());
}
There seem to be a few questions here.
1. Is it safe to handle a std::optional
in the body of a noexcept
function?
Yes, so long as the type T
, wrapped by std::optional
, is nothrow copy constructible.
In this code:
if (ox)
return {ox.value() * 2};
since you are checking before calling std::optional::value()
, an exception will never be thrown. That being said, since you're already sure that the ox.has_value()
, it's better to use the operator*
:
if (ox)
return {*ox * 2};
That way the compiler won't need to generate the precondition check and the throw std::bad_optional_access
statement (in simple cases the compiler can optimize that out based on if (ox)
but why make the compiler do more work).
2. Is this an efficient technique for returning computations? Can it avoid some exception bloat?
Presumably, by "exception bloat" you mean the binary size overhead from all the exception handling code that the compiler needs to generate. If for whatever reason you really care about this, then yes - the std::optional
technique can avoid that bloat at the cost of more overhead when no errors occur.
That's the trade-off you have to accept with this style error handling (std::optional
, std::error_code
, Outcome, etc.). You agree to a constant overhead on success in order to get constant overhead on failure. Exceptions, on the other hand, only incur an overhead (non-deterministic in time and space) on failure.
In this particular case you would probably be better off not polluting a simple function that otherwise cannot fail with error handling. Instead defer that to the caller. After all the caller might already know that the value exists.
3. Can this cause any trouble?
The main issue here is the lack of any information about the error.
std::optional
can't convey the reason the operation failed. In a trivial application this may not be an issue, but as soon as you start composing more complex operations, problems with tracking down the cause of failure will become apparent.
Even in this code there are a few error conditions that may be useful to report appropriately: IO errors and parsing errors. And then among parsing errors:
int
.Assuming you end up not using exceptions, consider using something like the proposed std::expected or the Outcome library.
I wouldn't recommend just using a std::variant
(at least not without wrapping it) as it does not convey the intent of error handling. Also, it doesn't support holding void
.