Hacker News new | ask | show | jobs
by jeroenhd 410 days ago
The result type does make for some great API design, but SerenityOS shows that this same paradigm also works fine in C++. That includes something similar to the ? operator, though it's closer to a raw function call.

SerenityOS is the first functional OS (as in "boots on actual hardware and has a GUI") I've seen that dares question the 1970s int main() using modern C++ constructs instead, and the API is simply a lot better.

I can imagine someone writing a better standard library for C++ that works a whole lot like Rust's standard library does. Begone with the archaic integer types, make use of the power your language offers!

If we're comparing C++ and Rust, I think the ease of use of enum classes/structs is probably a bigger difference. You can get pretty close, but Rust avoids a lot of boilerplate that makes them quite usable, especially when combined with the match keyword.

I think c++, the language, is ready for the modern world. However, c++, the community, seems to be struck at least 20 years in the past.

5 comments

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

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.

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.
> I think c++, the language, is ready for the modern world. However, c++, the community, seems to be struck at least 20 years in the past.

Good point. A language that gets updated by adding a lot of features is DIVERGING from a community that has mostly people that still use a lot of the C baggage in C++, and only a few folks that use a lot of template abstraction at the other end of the spectrum.

Since in larger systems, you will want to re-use a lot of code via open source libraries, one is inevitably stuck in not just one past, but several versions of older C++, depending on when the code to be re-used was written, what C++ standard was stable enough then, and whether or not the author adopted what part of it.

Not to speak of paradigm choice to be made (object oriented versus functional versus generic programmic w/ templates).

It's easier to have, like Rust offers it, a single way of doing things properly. (But what I miss in Rust is a single streamlined standard library - organized class library - like Java has had it from early days on, it instead feels like "a pile of crates").

Just give Rust 36 years of field use, to see how it goes.
36 years is counting from the first CFront release. Counting the same way for Rust, it's been around since 2006. It's got almost 20 years under it's belt already.

edit: what's with people downvoting a straight fact?

Rust 0.1, the first public release, came out in January 2012. CFront 1.0, the first commercial release, came out in 1985.

The public existence of Rust is 13 years, during which computing has not changed that much to be honest. Now compare this to the prehistory that is 1985, when CFront came out, already made for backwards compatibility with C.

I grew up with all the classic 8 bit micros, and to be honest, it doesn't feel like computing has changed at all since 1985. My workstation, while a billion times faster, is still code compatible with a Datapoint 2200 from 1970.

The memory model, interrupt model, packetized networking, digital storage, all function more or less identically.

In embedded, I still see Z80s and M68ks like nothing's changed.

I'd love to see more concrete implementations of adiabatic circuits, weird architectures like the mill, integrated FPGAs, etc. HP's The Machine effort was a rare exciting new thing until they walked back all the exciting parts. CXL seems like about the most interesting new thing in a bit.

Does GPU thingy count as something that has changed with computing?
Today a byte is 8 bits. That was not always the case back then, for example.
Because it is counting since CFront 2.0, the first official release with industry use in UNIX systems.

So that would be Rust 1.0, released in 2015, not 2006, putting it down to a decade.

And the point still stands when looking at any long enough ecosystem still in use, with strong backwards compatibility, not only the language, the whole ecosystem, eventually editions alone won't make it, and just like those languages, Rust will gain its own warts.

Fair enough. I can cop to getting the CFront date wrong. Still, a decade since 1.0 is non-trivial.

> eventually editions alone won't make it, and just like those languages, Rust will gain its own warts.

That's possible. Though C++ hasn't had editions, or the HLIR / MIR separation, the increased strictness, wonderful tooling, or the benefit of learning from the mistakes made with C++. Noting that, it seems reasonable to expect Rust to collect less cruft and paint itself into fewer corners over a similar period of time. Since C++ has been going for 36 years, it seems Rust will outlive me. Past that, I'm not sure I care.

C++ editions are -std=something, people keep forgeting Rust editions are quite limited in what they actually allow in grammar and semantic changes across versions, and they don't cover standard library changes.

IDEs are wonderful tooling, maybe people should get their heads outside UNIX CLIs and MS-DOS like TUIs.

Then there is the whole ecosystem of libraries, books, SDKs and industry standards.

A lot of people using C++ don't actually use any libraries. I've observed the opposite with Rust.

People choose C++ because it's a flexible language that lets you do whatever you want. Meanwhile Rust is a constrained and opinionated thing that only works if you do things "the right way".

> People choose C++ because it's a flexible language that lets you do whatever you want.

You went on a bit too long. C++ lets you do whatever. Whether you wanted that is not its concern. That's handily illustrated in Matt Godbolt's talk - you provided a floating point value but that's inappropriate? Whatever. Negative values for unsigned? Whatever.

This has terrible ergonomics and the consequences were entirely predictable.

It's just strong typing. You can do it in C++ too.
I’ve seen it argued that, in practice, there’s two C++ communities. One is fundamentally OK with constantly upgrading their code (those with enterprise refactoring tools are obviously in this camp, but it’s more a matter of attitude than technology) and those that aren’t. C++ is fundamentally caught between those two.
This is the truth. I interview a lot of C++ programmers and it amazes me how many have gone their whole careers barely touching C++11 let alone anything later. The extreme reach of C++ software (embedded, UIs, apps, high-speed networking, services, gaming) is both a blessing and a curse and I understand why the committee is hesitant to introduce breaking changes at the expense of slow progress on things like reflection.
C++ carries so much on its back and this makes its evolution over the past decade even more impressive.
Yes, people keep forgeting C++ was made public with CFront 2.0 back in 1989, 36 years of backwards compatibility, to certain extent.
C++ is C compatible so more than 50 years of backward compatibility. Even today the vast majority of C programs can be compiled as C++ and they just work. Often such programs run faster because C++ a few additions that the compiler can use to optimize better, in practice C programs generally mean the stronger rules anyway (but of course when they don't the program is wrong).
<pedantry corner>CFront was never compatible with K&R C to the best of my knowledge, so the actual start date would be whenever C89-style code in widespread use; I'm not sure how long before 1989 that was.
I can tell that during 1999 - 2003, the aC compiler we had installed on our HP-UX 11 development servers, still had issues with C89, we had #defines to use K&R C function declarations when coding on that system.
Kind of, compatible with C89 as language, and with C23 to the extent of library functions that can be written in that subset, or with C++ features.

And yes, being a "Typescript for C" born at the same place as UNIX and C, is both what fostered its adoption, among compiler and OS vendors, and also what brings some pains trying to herd cats to write safe code.

I created a library "cpp-match" that tries to bring the "?" operator into C++, however it uses a gnu-specific feature (https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html), I did support msvc falling-back to using exceptions for the short-circuit mechanism.

However it seems like C++ wants to only provide this kind of pattern via monadic operations.

You can't really do Try (which is that operator's name in Rust) because C++ lacks a ControlFlow type which is how Try reflects the type's decision about whether to exit early.

You can imitate the beginner experience of the ? operator as magically handling trivial error cases by "just knowing" what should happen, but it's not the same thing as the current Try feature.

Barry Revzin has a proposal for some future C++ (lets say C++ 29) to introduce statement expressions, the syntax is very ugly even by C++ standards but it would semantically solve the problem you had.