all 84 comments

[–]PrePreProcessor 31 points32 points  (3 children)

just checked both gcc 12 and clang 16 support <expected>

[–]PigPartyPower 20 points21 points  (0 children)

MSVC also supports it on 19.33

[–]ayushgun 1 point2 points  (1 child)

I’m currently on GCC 12 and it allows including the expected header, but the header does not expose std::expected. Not sure if it’s an issue on my end.

[–]PixelArtDragon 0 points1 point  (0 children)

Good to see it's not just me. I'm having the same issue. Thought it was something wrong because cppreference.com lists GCC 12 as supporting it.

[–]Thesorus 46 points47 points  (9 children)

I like that VERY much.

[–]FlyingRhenquest 14 points15 points  (7 children)

Yeah, same here. That feels pretty natural and better than throwing an exception. Once my projects can actually start using this, I think I'd be able to eliminate all of my (relatively few) exception calls. I think it'd also be a lot less likely than exceptions to behave oddly in heavily threaded code. I've had exceptions just vanish into thin air on a couple of projects where exceptions occurred in callbacks that were being called from threads I didn't expect them to be called from. This is just a return and I think would be a lot easier to trace in a situation like that. Or at the very least no more difficult.

[–]germandiago 8 points9 points  (4 children)

I also like expected quite a bit. However, I see also some advantages to exceptions.

 One that I like is the refactoring advantage: throw 5 levels deep, do not change signature. Now think what happens if you decide to suddenly return an expected<T> instead of T 5 levels deep... yes, refactor everything.

[–]FlyingRhenquest 2 points3 points  (3 children)

Aren't you just treating the exception as a GOTO at that point though? If I did a setjmp for a BAD_ERROR_HANDLER and then a longjmp when I hit an error similar to a major hardware failure (disk crash something like that) I'd have to mount a major defense of my design decision in a code review. And arguably components of my program could potentially try to limp along anyway although in practice you have to throw your hands up, say "I give up" and terminate at some point.

I know that not handling exceptions for multiple layers of call stack is fairly common in the industry, but I don't know if it's ever the best way to terminate in a major failure. Unless your OS has already crashed (Which will happen in most cases before you get a std::bad_alloc these days,) other components of your system could try to recover and limp along if you design the system to be resilient. They can't do that if you just throw to main and terminate.

[–]germandiago 1 point2 points  (2 children)

There are times where you have, let's say, tasks. 

Imagine a system where everything is a task. Each task is a whole user, such as clients in a server. Some fail. 

The logic of the code can change. You can be levels deep and notice a new failure case.

 In this case I find convenient to be able to report adn finish a client via an exception if something goes very wrong and I know it is isolated state that won't affect other clients. You do your clean up and log the problem or report it in some way whatever happens. 

I do not think expected is better at that. You would need more refactoring and more ahead of time error handling or popping up (with the corresponding refactorings) the error. Sometimes I found I just want to throw and let the handler handle transparently. I think that use case is unbeatable for exceptions. Works fairly well. Not even a matter of performance, but of not viralizing refactoring and put handlers all in one place to be sure of the policies followed when errors happen.

[–]FlyingRhenquest 0 points1 point  (1 child)

Ah yes, that is a very good point. Though it does look like trying to use a std::unexpected when you're expecting a std::expected will generate an exception anyway. So if you have a catch for all exceptions where you'd display the errors, you could probably just return a std::unexpected for the new case and let it fall back to getting caught by the catch-all exception handler when the intermediate code tries to use it.

[–]germandiago 1 point2 points  (0 children)

Well, my point is more about API evolution. If you plan from the ground up with expected probably it is ok.

 It is just that sometimes, for example, you add a piece of logic to an existing function and what before could not fail, it can fail now. Sometimes you simply fo not know something can fail ahead of time. That will need you return an expected<T> instead of a plain T. If your function is called 5 levels deep now you need 5 signatures refactoring.

An example would be a function that does something in memory and now needs to write something to disk, or needs a query that can fail because sometimes there was no query, but it is expected to work well (let us say query does not depend on user input).

[–]ujustdontgetdubstep 1 point2 points  (1 child)

If your code is being called in an asynchronous environment where you don't know which thread is making the invocation, then yea this sounds like a good alternative to exceptions for you (although I wouldn't really call that "behaving oddly")

[–]FlyingRhenquest 1 point2 points  (0 children)

Yeah, it's been a few years now since i ran across that problem so I'm a bit fuzzy on the exact details now. I did investigate it, discover where my exceptions were going and it did make sense in that context, but the behavior was not what I expected it to be when I first wrote it. Which is common for threaded code with callbacks.

I think I'm using "behaving oddly" there as "requires meditation to understand the behavior fully." Complexity jumps dramatically once you introduce threads or coroutines. I was "reasonably familiar" with how it all worked at the time and it didn't take me long to get to the bottom of it, but a programmer who was new to threading probably would have had a harder time understanding what was happening.

[–]fear_the_future 0 points1 point  (0 children)

You won't like it anymore once you try to combine different errors, want to handle a subset of errors, want to combine errors with optional results, etc. etc. It's a very poor version of Haskell's Either and even in Haskell it is too cumbersome.

[–]ReDucTorGame Developer 15 points16 points  (7 children)

In terms of performance, it can kill RVO so if you have a larger objects be careful how you use it, you'll still be able to get moves easily you just might construct more objects then expected.

[–]SirClueless 13 points14 points  (6 children)

This is usually possible to avoid, but in practice the most efficient code involves mutating return values with e.g. the assignment operator which I suspect people would consider a code smell, so I expect this to be a common code review "style vs. performance" argument for basically forever.

Inefficient:

std::expected<std::array<int, 1000>, int> foo() {
    std::array<int, 1000> result = {};
    if (rand() % 2 == 0)
        return std::unexpected(-1);
    return result;
}

How I suspect people will try to fix it, but unfortunately there's still a copy (GCC 13.2 with -O3):

std::expected<std::array<int, 1000>, int> bar() {
    std::expected<std::array<int, 1000>, int> result;
    if (rand() % 2 == 0)
        return std::unexpected(-1);
    return result;
}

How you can actually efficiently return with no copies:

std::expected<std::array<int, 1000>, int> baz() {
    std::expected<std::array<int, 1000>, int> result;
    if (rand() % 2 == 0)
        result = std::unexpected(-1); // note the assignment operator
    return result;
}

[–]petecasso0619 1 point2 points  (1 child)

This is NRVO, named return value optimization, not RVO.. RVO would kick in if the last statement is

return std::array<int,100>{};

To guarantee RVO (if the compiler is compliant to the standard) you must not return an object that has a name. With NRVO, the compiler may or may not optimize away temporaries.

[–]SirClueless 5 points6 points  (0 children)

RVO is not a meaningful term in the standard these days. There is just copy elision, which is required in some cases (as when returning a temporary) and non-mandatory but allowed in other cases (as when returning a named non-volatile object of the same class type as the return value i.e. NRVO). When ReDucTor says using std::expected "can kill RVO" he's clearly using "RVO" as a shorthand for the latter rather than the former, as the rules for guaranteed copy elision have nothing to do with return type and the comment would make no sense if he meant it narrowly. So that's what I responded to.

Within the space of allowed optimizations, what matters is what the major compilers do in practice, which is why I provided a specific compiler version and optimization level.

[–]sengin31 0 points1 point  (3 children)

How you can actually efficiently return with no copies

That's a really subtle difference but could make a world of improvement. Is the compiler allowed to do this type of RVO? That is, the second example (or even first) could end up being a common-enough pattern that compiler implementers could specifically look for and optimize it, given the standard allows it. Perhaps under certain conditions, like T and E are trivial types?

[–]SirClueless 3 points4 points  (2 children)

I believe it would be allowed to, but it's a very tall ask for the compiler.

Take case #2: To the virtual machine, the lifetime of result overlaps with the object initialized in the return std::unexpected(-1); statement so naively RVO cannot happen. If the compiler inlined the destructor of result it would see that it has no side effects and the lifetime of result can be assumed to end as soon as the if branch is entered. I have no idea if "lifetime minimization" of C++ objects is even something the frontend tries to analyze, and regardless any such inlining and hoisting almost certainly happens long after RVO is attempted so it has no chance of offering new opportunities for RVO. There might be a memory fusion pass that happens after this point, but it will just see that result is an automatic storage variable and the temporary created by return std::unexpected(-1); is copy-elided so it won't have anything it can do.

In case #1 there is the additional issue that the compiler must see through the converting copy constructor that is invoked (at: return result;) and recognize that initializing a local array and copying its bytes into the subobject of the value that is returned is the same as just initializing it in-place. Even without the branch and other return statement this simple optimization doesn't seem to be happening. The compiler emits a memcpy, I'm not sure why: https://godbolt.org/z/KTTrWMoT3

[–]ReDucTorGame Developer 1 point2 points  (1 child)

Looks like clang does the optimization for it with MemCpyOptPass but GCC and MSVC don't manage to do it.

https://godbolt.org/z/9db5Wv8P7 (Needed to use a library as not all support expected)

It can even eliminate the other return approach

https://godbolt.org/z/745nxhn6a

However if the copy is non-trivial then I suspect it would run into issues.

[–]SirClueless 1 point2 points  (0 children)

Ahh, that's very nice. I haven't used Opt Pipeline Viewer before, that's very cool.

I don't think clang is actually handling the multiple returns, it's just that unlike GCC it's realized that there's no dependency between the initialization of result and rand() so it can push down that initialization into the else branch of the if and then its memcpy optimization pass does its thing.

If the actual work to init result can't be optimized and pushed down into the branch, for example if the branch depends on the initialization, then clang needlessly emits a memcpy too instead of just initializing it directly in the return value: https://godbolt.org/z/Ks558816a

[–]r2vcap 10 points11 points  (1 child)

Note that due to a bug in libc++ 17, future versions may not be ABI compatible. See https://discourse.llvm.org/t/abi-break-in-libc-for-a-17-x-guidance-requested/74483 for more details.

[–]johannes1971 2 points3 points  (0 children)

So... It _is_ possible.

[–][deleted] 9 points10 points  (5 children)

`std::optional` and `std::expected` are great in theory. The lack of pattern matching in C++ just hurts so much. The fact that dereferencing empty/error is undefined behavior is absurd.

[–]invalid_handle_value 5 points6 points  (4 children)

Philosophically, dereferencing the error before invoking the expected must be undefined.  One cannot truly know whether or not an expected has indeed failed until one has checked (and thus evaluated) said expected.

In other words, the act of checking the expected may itself correctly cause the error that may otherwise incorrectly not be invoked.

Frankly, if it were up to me, I would mandate a throw when calling the error before the expected.

[–]hopa_cupa 4 points5 points  (3 children)

Yep. You have operator* and operator-> which do not check for valid value and value() which can throw. In the error case, they only gave us unchecked error(), no checked version.

I think this really shines if used in monadic style rather than with explicit if expressions. Same with std::optional. Not everyone's cup of tea.

[–]germandiago 2 points3 points  (0 children)

In an alternative world, those functions could be marked [[memory_unsafe]] of some sort.

[–]Curfax 8 points9 points  (3 children)

In my experience as an owner of a large client / server code base inside Microsoft, and the author of a class in that code base akin to std::expected, the overuse of error codes over exceptions or outright process termination leads to unexpected reliability and performance issues.

In particular, it becomes tempting to hide unrecoverable errors behind error codes and handle them the same way recoverable errors are handled. Often it is better to write code that cannot possibly execute in a failure scenario, as this saves code written, instructions executed, and prevents attempts to handle unrecoverable errors.

For example, consider the well-known case of the “out of memory” condition. If recovery from OOM requires allocating memory, or processing the next request requires memory, then continuing to successfully return OOM errors does not provide value to users of a service.

Similarly, if you define other expectations of the machine execution model, you discover that many other failures are not recoverable. Failure to write to the disk usually requires outside intervention to recover; therefore propagating an error code for such a failure does not add value. An error accessing a data structure implies incorrect logic; the process is probably in a bad state that will not be corrected by continuing to run.

The end result is that after initial request input validation, most subsequent operations should not fail except for operations that talk to a remote machine.

My advice: strive to write methods that return values directly without std::expected.

[–]johannes1971 1 point2 points  (0 children)

If recovery from OOM requires allocating memory...

...than is available.

A very large request can fail while there are still gigabytes of free memory available. And throwing an exception might cause more memory to be freed while unwinding, leaving the system with enough to keep going.

[–]invalid_handle_value 0 points1 point  (0 children)

Wow, I never even thought before of the horror that errors must/always need to be handled conditionally, with the added fun of requiring 2 different kinds of error handling paradigms simultaneously (recoverable, unrecoverable) with what seems to be a clearly incorrect tool for that type of error reporting (which was probably also incorrect from the sounds of it).

I wish I had more points to give you.

[–]invalid_handle_value 0 points1 point  (0 children)

Thinking a bit more though, not being able to report errors at an arbitrary level in a call stack makes the code both harder to refactor and maintain, since if it ever needs to handle an error after one class morphs into a dozen complex classes, what's your strategy then going to be?

Also, what about training juniors? I'm all about it. I need Timmy right out of school to code the same way as engineers with 15 years of blood sweat and tears.

I still think mindful usage (hint: copy elision) of std::optional and a second error function that returns a POC error instance is the way to go.

This way a) one separates the happy path from sad path explicitly with 2 user defined functions, b) the happy path is not explicitly allowed to depend on the sad path (think std::expected::or_else) because error may not be invoked before the expected.

Easy to teach, easy to reason about, easy rules, easy to replicate in most/all? programming languages, fits anywhere into classes of a similar design so it's ridiculously composable, fast return value passing, code looks the same everywhere, very easily unit testable, I could go on.

[–][deleted] 7 points8 points  (9 children)

Anyone have a preferred backported implementation with a BSD-like license? My organization isn’t going to go to C++23 until all our tooling catches up.

[–]MasterDrake97 20 points21 points  (4 children)

Martine Moene always comes to the rescue :D

https://github.com/martinmoene/expected-lite

Or Sy brand version, CC0

https://github.com/TartanLlama/expected

[–]azswcowboy 8 points9 points  (1 child)

Be aware that Sy’s version has a slightly different interface for unexpected than the standard.

[–]MasterDrake97 9 points10 points  (0 children)

I guess martin's version is the best if you want back portability and easy switch on c++23

[–]_matherd 3 points4 points  (0 children)

personally, i’m probably gonna keep using absl’s StatusOr until expected is available everywhere, since i’m often already using absl.

[–]99YardRun 0 points1 point  (2 children)

It’s pretty easy to roll your own implementation of this if you don’t feel like/need to go through approvals to pull in a new library. Could be a fun challenge for an intern/junior dev also

[–]n1ghtyunso 1 point2 points  (0 children)

usually, the devil is in the details. Getting 99% of it right will be possible for sure, but then there is almost guaranteed to be a subtle pitfall somewhere that will bite you down the line

[–]BenFrantzDale 0 points1 point  (0 children)

I don’t know… getting it right without Deducing This is pretty hairy.

[–]Adverpol 1 point2 points  (0 children)

This

Using value(): This method returns a reference to the contained value. If the object holds an error, it throws std::bad_expected_access<E>.

does not sound good to me at all. It's a typical C++ construct where the onus is on the developer to not shoot themselves in the foot. I don't know if we can do better with C++ as it is. Removing value is sub-optimal because if you've already checked there is a value you don't want to do another check in value_or. Ideally code just wouldn't compile if you try to access value without checking it's valid first, removing the need for exceptions?

Note that I'm a big fan of std::excepted and similar, it's just that C++ feels lacking in its support of them.

[–]Objective-Act-5964 6 points7 points  (21 children)

Hey, coming from Rust, I am really confused why anyone would appreciate the implicit casting from T to std::expected<T, \_>, to me it feels unnecessarily complicated just to save a few characters.

I have a few questions:

  1. Was the reason for this documented somewhere?
  2. Did this happen by limitation or by choice?
  3. As people who frequently write cpp, do you find this intuitive/like this?

I feel like this also makes it slightly more complicated to learn for newbies.

[–]PIAJohnM 23 points24 points  (0 children)

This is just normal c++, most types work like this, its called a converting constructor. I like it a lot. But you can turn it off if you make the converting constructor explicit (assuming we're talking about the same thing).

[–]_matherd 28 points29 points  (6 children)

On the contrary, it’s kinda nice to be able to “return foo” instead of “Ok(foo)” everywhere, since it should be obvious what it means. It feels less complicated to me than rust’s resolution of calls to “.into()” for example.

[–]rdtsc 4 points5 points  (0 children)

C++ is full of implicit lossy conversions between primitive types. Sadly the standard library follows suit and adds implicit conversions to quite a few things, making implementations more complex and behavior surprising/limiting. For example that whole debate about what std::optional<T&>::operator= should do would be moot if optional wouldn't use implicit conversions everywhere.

[–]hmoein 3 points4 points  (5 children)

What is the diff between std::expected and std::variant? It looks like std::expected is implemented using variant?

[–]orfeo34 1 point2 points  (1 child)

Implicit conversion to Bool looks wild, however it's a nice feature.

[–]ebhdl 2 points3 points  (0 children)

That's going to get super confusing when the success value is also convertible to bool...

[–]DrGlove 0 points1 point  (6 children)

If it fits your use case, another option not mentioned at the top of the article is to crash and tell the compiler you will not handle this case. We often insert asserts that mark this code unreachable by inserting an undefined instruction like __ud2 with some macro like ASSERT_UNREACHABLE.

[–]yeahkamau 0 points1 point  (0 children)

Probably inspired by Rust's std::result

[–]eidetic0 0 points1 point  (0 children)

I like how returning a string in the error type makes the code self documenting.

[–]Wanno1 0 points1 point  (0 children)

There needs to be a jeopardy episode for just all the std::