Hacker News new | ask | show | jobs
by jchw 410 days ago
Google has been doing a very similar, but definitely somewhat uglier, thing with StatusOr<...> and Status (as seen in absl and protobuf) for quite some time.

A long time ago, there was talk about a similar concept for C++ based on exception objects in a more "standard" way that could feasibly be added to the standard library, the expected<T> class. And... in C++23, std::expected does exist[1], and you don't need to use exception objects or anything awkward like that, it can work with arbitrary error types just like Result. Unfortunately, it's so horrifically late to the party that I'm not sure if C++23 will make it to critical adoption quickly enough for any major C++ library to actually adopt it, unless C++ has another massive resurgence like it did after C++11. That said, if you're writing C++ code and you want a "standard" mechanism like the Result type, it's probably the closest thing there will ever be.

[1]: https://en.cppreference.com/w/cpp/utility/expected

3 comments

I had a look. In classic C++ style, if you use *x to get the ‘expected’ value, when it’s an error object (you forgot to check first and return the error), it’s undefined behaviour!

Messing up error handling isn’t hard to do, so putting undefined behaviour here feels very dangerous to me, but it is the C++ way.

The reason it works this way is there's legitimately no easy way around it. You're not guaranteed a reasonable zero value for any type, so you can't do the slightly better Go thing (defined behavior but still wrong... Not great.) and you certainly can't do the Rust thing, because... There's no pattern matching. You can't conditionally enter a branch based on the presence of a value.

There really is no reasonable workaround here, the language needs to be amended to make this safe and ergonomic. They tried to be cheeky with some of the other APIs, like std::variant, but really the best you can do is chuck the conditional branch into a lambda (or other function-based implementation of visitors) and the ergonomics of that are pretty unimpressive.

Edit: but maybe fortune will change in the future, for anyone who still cares:

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p26...

You could assert. You could throw. I can’t understand how, this modern age where so many programs end up getting hacked, that introducing more UB seems like a good idea.

This is one odd the major reasons I switched to rust, just to escape spending my whole life worrying about bugs caused by UB.

Assertions are debug-only. Exceptions are usually not guaranteed to be available and much of the standard library doesn't require them. You could std::abort, and that's about it.

I think the issue is that this just isn't particularly good either. If you do that, then you can't catch it like an exception, but you also can't statically verify that it won't happen.

C++ needs less of both undefined behavior and runtime errors. It needs more compile-time errors. It needs pattern matching.

I agree these things would be better, but I don’t understand how anyone can think UB is better than abort.

(Going to moan for a bit, and I realise you aren’t responsible for the C++ standards mess!)

I have been hearing for about… 20 years now that UB gives compilers and tools the freedom to produce any error catching they like, but all it seems to have done in the main is give them the freedom to produce hard to debug crash code.

You can of course usually turn on some kind of “debug mode” in some compilers, but why not just enforce that as standard? Compilers would still be free to add a “standards non-compliant” go fast mode if they like.

> but why not just enforce that as standard

I don’t think people want that as standard. The whole point of using C++ tends to be because you can do whatever you need to for the sake of performance. The language is also heavily driven by firms that need extreme performance (because otherwise why not use a higher level language)

There are knobs like stdlib assertions and ubsan, but that’s opt-in because there’s a cost to it. Part of it is also the commitment to backwards compatibility and code that compiled before should generally compile now (though there are exceptions to that unofficial rule).

A lot UB is things you wouldn't do anyway. While it is possible to define divide by zero or integer overflow, what does it mean. If you code does either of those things you have a bug in your code (a few encryption algorithms depend on specific overflow behavior - if your language promises that same behavior it is useful).

Since CPUs handle such things differently whatever you define to happen means that the compiler as to insert a if to check on any CPU that doesn't work how you define it - all for something that you probably are not doing. The cost is too high in a tight loop when you know this won't even happen (but the compiler does not).

No

Culturally, I think C++ has a policy of "there's no single right answer." Which leads to there being no wrong answers. We just need more answers so everyone's happy. Which is worse.
Abort would be fine here. Operator* on expected is intended to be used when you have already verified the result wasn’t error.
Of course you can do the Rust thing, it's just taking a function object.
`StatusOr<T>::operator` there is akin to `Result<T, _>::unwrap()`. On C++ unwrapping looks like dereferencing a pointer which is scary and likely UB already.

But as you learn to work with StatusOr you'll end up just using just ASSIGN_OR_RETURN everytime and dereferencing remains scary. I guess the complaint is that C++ won't guarantee that the execution will stop, but that's the C++ way after you drop all safety checks in `StatusOr::operator` to gain performance.

This is the idiomatic way in C++. I'm not even sure what your proposed alternative is -- as other commenters have noted, an exception or "panic" are not actual options.

Every pointer dereference, array access, and even integer truncation is UB in C++. This isn't rust.

A static analyzer can and does catch these errors and others internally. Typical usage of StatusOr is via macros like ASSIGN_OR_RETURN and RETURN_IF_ERROR; actually using the * operator would definitely draw my attention in code review.

Very similar footgun on std::optional::operator*. The big C++ libraries do at least have (debug-only) assertions on misuse.
There’s a few backports around, not quite the same as having first class support, though.
I believe the latest versions of GCC, Clang, MSVC and XCode/AppleClang all support std::expected, in C++23 mode.
Facebook's Folly has a similar type: folly::Expected (dating to 2016).
If I had to guess, that idea came from Andrei Alexandrescu.