0

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());
}
  • Is this an efficient techinique for returning computations? Can it avoid some exception bloat?
  • Can this cause any trouble?
Nazinho
  • 311
  • 2
  • 8
  • 1
    Are you asking particularly for `std::optional` or, generally, for `std::optional` where `T` some type with potentially-throwing construtor? – Daniel Langr Sep 24 '19 at 06:10
  • T with potentially throwing constructor. Though I hope most types will have a noexcept move ctor and that any necessary copy happens on caller site – Nazinho Sep 24 '19 at 14:22

2 Answers2

4

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:

  • the input may not be a valid number;
  • the number may exceed the range of 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.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
andrey
  • 463
  • 2
  • 8
  • 1
    "can avoid that bloat at the cost of more overhead" now that"s an interesting formulation – n. m. could be an AI Sep 24 '19 at 09:18
  • @n.m. Well yes) you would be avoiding code bloat from exceptions (in one place) at the cost of more overhead when no error occurs (somewhere else). – andrey Sep 24 '19 at 09:26
  • 2
    My spellchecker corrects this to "there is overhead either way and you need to measure in order to find out wich one gives less of it in your particular application". – n. m. could be an AI Sep 24 '19 at 09:31
  • @n.m. Exactly, couldn't agree more – andrey Sep 24 '19 at 10:04
  • I understand your statement about no error information but in such simple case I think it's enough to either return something or nothing. – Nazinho Sep 24 '19 at 14:03
  • The caller could check whether the input value exists or not before unwrapping it and calling a function taking a int. The problem is that this don't compose well with monadic structures. You're right to assume that this potentially adds a constant overhead wherever it's used but I expect the compiler to be smart enough to optimize it out whenever possible (zero-cost abstractions). – Nazinho Sep 24 '19 at 14:17
  • @Nazinho "I expect the compiler to be smart enough to optimize it out whenever possible". Sadly in the real world we have things like non-inline functions, builds without LTO, shared libraries, and compilers that can only optimize the simplest of cases ([some are better than others of course](https://godbolt.org/z/hKfwfa)). Of course in most cases a few extra cycles aren't the end of the world - so always measure) – andrey Sep 24 '19 at 15:24
  • @andrey Not very on topic but is this 'sad reality' inherent to C++'s ecosystem? Rust have the same concept under a slightly different name Option, yet it is used not sparingly in Rust codebases and is considered to be a zero-cost abstraction – Nazinho Sep 24 '19 at 23:24
  • 1
    @Nazinho I would guess that `Option` in Rust has roughly the same overhead as a `std::optional` (might be easier on the compiler as it's a language-based `enum`). I doubt it's actually considered a *zero-cost* abstraction. It may be a *zero-overhead* abstraction though (same as `std::optional`), i.e. you couldn't write it better by hand. It still has a cost over the wrapped type `T`. So I'm not saying "use sparingly", more like: use when it makes sense for the API, i.e. don't make the function do work that is doesn't need to do. Maybe make a wrapper function for that. Maybe even a generic one – andrey Sep 29 '19 at 18:31
  • After reading some answers from [this question](https://stackoverflow.com/questions/43130192/in-rust-is-option-compiled-to-a-runtime-check-or-an-instruction-jump) also on SO, it seems like you may be right in regards to `Option` being a zero-cost abstraction. Though "It may be a zero-overhead abstraction [...] i.e. you couldn't write it better by hand." is probably [not the case](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/optional), at least for libstc++ – Nazinho Sep 29 '19 at 19:30
  • @Nazinho Of course I meant "... i.e. you couldn't _reasonably_ write it better by hand", There are definitely some corner cases, where one could write it better by hand for a specific type, if one can intrude on that type for instance. But for the generic case, what's wrong with the libstdc++ implementation of `std::optional`? – andrey Oct 01 '19 at 16:29
  • @andrey It's not that there's something wrong with the implementation, just that it's very messy. [libcxx's](https://github.com/llvm-mirror/libcxx/blob/master/include/optional) looks clearer, at least to me. Also, `std::optional` does/permits a lot of implicit type coercion which I'm not particulary fond of – Nazinho Oct 01 '19 at 18:46
  • @andrey Also, after reading the code, it seems like if your `T` in `std::optional` is `is_nothrow_destructible_v == false` for some reason, any reset operation that would result in the destruction of the contained value may call `std::terminate`. True for both [clang](https://wandbox.org/permlink/begicuFtH4HDVMD1) and [gcc](https://wandbox.org/permlink/pFVata3saizV9psj) – Nazinho Oct 01 '19 at 19:12
  • @Nazinho OK, so obviously one can dislike the way `std::optional` is designed, but "you couldn't reasonably write it better by hand" is about overhead, not style (implicit conversions etc.). "... any reset operation that would result in the destruction of the contained value may call `std::terminate`" - that's fine because [such a `T` violates](https://en.cppreference.com/w/cpp/utility/optional) *Destructible*: *T - [...] The type must meet the requirements of Destructible*. And *Destructible* says: *`u.~T()`: All resources owned by `u` are reclaimed, no exceptions are thrown.* – andrey Oct 02 '19 at 15:30
1

Exceptions are a touchy topic in C++ with several different opinions about it. I'm aware that there are standardization proposals in flight (c++23 or later) to improve a lot about them and you are right that they ain't zero overhead.

However, no system will be completely zero overhead as you do need error handling and checking at several places.

To me, using std::optional is plain wrong, because there ain't any information: It went wrong, please attach a debugger to find out why. I'm more in favor of a exception like structure that has either a value or a failure info. std:: variant comes to mind. Off course, without reporting it, you still need a debugger to understand what went wrong.

That said, if you don't mind this, your code will be fine for functionality and performance. What I do wonder is: do you need to use optional as an argument? You currently chain one after the other, and with function overloading, you could provide a smaller implementation:

// This function doesn't have state
 int multiply_by_2(int ox) noexcept
{
    return ox * 2;
}

// This function doesn't have state
std::optional<int> multiply_by_2(std::optional<int> ox) noexcept
{
    if (ox)
        return multiply_by_2(ox.value());

    // empty input case
    return std::nullopt;
}

This way, you have less overhead if you're input can't be a failure.

Finally: make sure these methods are visible to the caller. As you don't check your error path, you would have UB in case of failure. I haven't checked, though compilers might be able to remove the failure path through your function as it cannot be used in a confirming program.

JVApen
  • 11,008
  • 5
  • 31
  • 67
  • 1
    I don't follow this logic. When the user is the programmer, "attach a debugger" is a valid approach. If the user is not a programmer, "attach failure information to a `std::variant`" is not going to help. – MSalters Sep 24 '19 at 11:26
  • I should update the answer, its not just about having the info, it's also about reporting it. (I thought it was implied) – JVApen Sep 24 '19 at 20:49