←back to thread

611 points LorenDB | 1 comments | | HN request time: 0.198s | source
Show context
dvratil ◴[] No.43908097[source]
The one thing that sold me on Rust (going from C++) was that there is a single way errors are propagated: the Result type. No need to bother with exceptions, functions returning bool, functions returning 0 on success, functions returning 0 on error, functions returning -1 on error, functions returning negative errno on error, functions taking optional pointer to bool to indicate error (optionally), functions taking reference to std::error_code to set an error (and having an overload with the same name that throws an exception on error if you forget to pass the std::error_code)...I understand there's 30 years of history, but it still is annoying, that even the standard library is not consistent (or striving for consistency).

Then you top it on with `?` shortcut and the functional interface of Result and suddenly error handling becomes fun and easy to deal with, rather than just "return false" with a "TODO: figure out error handling".

replies(24): >>43908133 #>>43908158 #>>43908212 #>>43908219 #>>43908294 #>>43908381 #>>43908419 #>>43908540 #>>43908623 #>>43908682 #>>43908981 #>>43909007 #>>43909117 #>>43909521 #>>43910388 #>>43912855 #>>43912904 #>>43913484 #>>43913794 #>>43914062 #>>43914514 #>>43917029 #>>43922951 #>>43924618 #
jeroenhd ◴[] No.43908294[source]
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.

replies(5): >>43908844 #>>43909517 #>>43909952 #>>43911784 #>>43913462 #
jchw ◴[] No.43908844[source]
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

replies(3): >>43909211 #>>43910401 #>>43910458 #
CJefferson ◴[] No.43910458[source]
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.

replies(4): >>43910534 #>>43915109 #>>43926840 #>>43947605 #
jchw ◴[] No.43910534[source]
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...

replies(2): >>43910726 #>>43913215 #
CJefferson ◴[] No.43910726[source]
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.

replies(1): >>43910876 #
jchw ◴[] No.43910876[source]
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.

replies(3): >>43911317 #>>43914487 #>>43947592 #
CJefferson ◴[] No.43911317[source]
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.

replies(2): >>43914831 #>>43915123 #
bluGill ◴[] No.43915123[source]
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

replies(1): >>43918452 #
jchw ◴[] No.43918452[source]
This is a bad answer too, IMO.

I think there is a solid case for the existence of undefined behavior; even Rust has it, it's nothing absurd in concept, and you do describe some reasoning for why it should probably exist.

However, and here's the real kicker, it really does not need to exist for this case. The real reason it exists for this case is due to increasingly glaring deficiencies in the C++ language, namely, again, the lack of any form of pattern matching for control flow. Because of this, there's no way for a library author, including the STL itself, to actually handle this situation succinctly.

Undefined behavior indeed should exist, but not for common cases like "oops, I didn't check to see if there was actually a value here before accessing it." Armed with a moderately sufficient programming language, the compiler can handle that. Undefined behavior should be more like "I know you (the compiler) can't know this is safe, but I already know that this unsafe thing I'm doing is actually correct, so don't generate safeguards for me; let what happens, happen." This is what modern programming languages aim to do. C++ does that for shit like basic arithmetic, and that's why we get to have the same fucking CVEs for 20+ years, over and over in an endless loop. "Just get better at programming" is a nice platitude, but it doesn't work. Even if it was possible for me to become absolutely perfect and simply just never make any mistakes ever (lol) it doesn't matter because there's no chance in hell you'll ever manage that across a meaningful segment of the industry, including the parts of the industry you depend on (like your OS, or cryptography libraries, and so on...)

And I don't think the issue is that the STL "doesn't care" about the possibility that you might accidentally do something that makes no sense. Seriously, take a look at the design of std::variant: it is pretty obvious that they wanted to design a "safe" union. In fact, what the hell would the point of designing another unsafe union be in the first place? So they go the other route. std::variant has getters that throw exceptions on bad accesses instead of undefined behavior. This is literally the exact same type of problem that std::expected has. std::expected is essentially just a special case of a type-safe union with exactly two possible values, an expected and unexpected value (though since std::variant is tagged off of types, there is the obvious caveat that std::expected isn't quite a subset of std::variant, since std::expected could have the same type for both the expected and unexpected values.)

So, what's wrong? Here's what's wrong. C++ Modules were first proposed in 2004[1]. C++20 finally introduced a version of modules and lo and behold, they mostly suck[2] and mostly aren't used by anyone (Seriously: they're not even fully supported by CMake right now.) Andrei Alexandrescu has been talking about std::expected since at least 2018[3] and it just now finally managed to get into the standard in C++23, and god knows if anyone will ever actually use it. And finally, pattern matching was originally proposed by none other than Bjarne himself (and Gabriel Dos Reis) in 2019[4] and who knows when it will make it into the standard. (I hope soon enough so it can be adopted before the heat death of the Universe, but I think that's only if we get exceptionally lucky.)

Now I'm not saying that adding new and bold features to a language as old and complex as C++ could possibly ever be easy or quick, but the pace that C++ evolves at is sometimes so slow that it's hard to come to any conclusion other than that the C++ standard and the process behind it is simply broken. It's just that simple. I don't care what changes it would take to get things moving more efficiently: it's not my job to figure that out. It doesn't matter why, either. The point is, at the end of the day, it can't take this long for features to land just for them to wind up not even being very good, and there are plenty of other programming languages that have done better with less resources.

I think it's obvious at this point that C++ will never get a handle on all of the undefined behavior; they've just introduced far too much undefined behavior all throughout the language and standard library in ways that are going to be hard to fix, especially while maintaining backwards compatibility. It should go without saying that a meaningful "safe" subset of C++ that can guarantee safety from memory errors, concurrency errors or most types of undefined behavior is simply never going to happen. Ever. It's not that it isn't possible to do, or that it's not worth doing, it's that C++ won't. (And yes, I'm aware of the attempts at this; they didn't work.)

The uncontrolled proliferation of undefined behavior is ultimately what is killing C++, and a lot of very trivial cases could be avoided, if only the language was capable of it, but it's not.

[1]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n17...

[2]: https://vector-of-bool.github.io/2019/01/27/modules-doa.html

[3]: https://www.youtube.com/watch?v=PH4WBuE1BHI

[4]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p13...

replies(2): >>43918899 #>>43919900 #
1. nayuki ◴[] No.43919900[source]
I agree very much with what you wrote.

> the lack of any form of pattern matching for control flow

Growing features after the fact is hard. Look at the monumental effort to get generics into Go. Look at how even though Python 3.10 introduced the match statement, it is a statement and not an expression - you can't write `x = match ...`, unlike Rust and Java 14. So it doesn't surprise me that C++ struggles with this.

> Undefined behavior indeed should exist

Agreed. Rust throws up its hands in narrow cases ( https://doc.rust-lang.org/reference/behavior-considered-unde... ), and even Java says that calling Thread.stop() and forcing monitor unlocks can lead to corrupted data and UB.

> but not for common cases like

Yes, C/C++ have far, far too many UB cases. Even down to idiotically simple things like "failing to end a source file with newline". C and C++ have liberally sprinkled UB as a cop-out like no other language.

> C++ does that for shit like basic arithmetic

I spent an unhealthy amount of time understanding the rules of integer types and arithmetic in C/C++. Other languages like Rust are as capable without the extreme mental complexity. https://www.nayuki.io/page/summary-of-c-cpp-integer-rules

Oh and, `(uint16_t)0xFFFF * (uint16_t)0xFFFF` will cause a signed 32-bit integer overflow on most platforms, and that is UB and will eat your baby. Scared yet? C/C++ rules are batshit insane.

> "Just get better at programming" is a nice platitude, but it doesn't work.

Correct. Far too often, I hear a conversation like "C/C++ have too many UB, why can't we make it safer?" "Just learn to write better code, dumbass". No, literal decades of watching the industry tells us that the same mistakes keep happening over and over again. The evidence is overwhelming that the languages need to change, not the programmers.

> it's obvious at this point that C++ will never get a handle on all of the undefined behavior; they've just introduced far too much undefined behavior all throughout the language and standard library

True.

> in ways that are going to be hard to fix, especially while maintaining backwards compatibility

Technically not true. Specifying undefined behavior is easy, and this has already been done in many ways. For example, -fwrapv makes signed overflow defined to wrap around. For example, you could zero-initialize every local variable and change malloc() to behave like calloc(), so that reading uninitialized memory always returns zero. And because the previous behavior was undefined anyway, literally any substitute behavior is valid.

The problem isn't maintaining backward compatibility, it's maintaining performance compatibility. Allegedly, undefined behavior allows the compiler to optimize out redundant arithmetic, redundant null checks, etc. I believe this is what stops the standards committees from simply defining some kind of reasonable behavior for what is currently considered UB.

> a meaningful "safe" subset of C++ that can guarantee safety from memory errors, concurrency errors or most types of undefined behavior is simply never going to happen

I think it has already happened. Fil-C seems like a capable approach to transpile C/C++ and add a managed runtime - and without much overhead. https://github.com/pizlonator/llvm-project-deluge/blob/delug...

> The uncontrolled proliferation of undefined behavior is ultimately what is killing C++

It's death by a thousand cuts, and it hurts language learners the most. I can write C and C++ code without UB, but it took me a long time to get there - with a lot of education and practice. And UB-free code can be awkward to write. The worst part of it is that the knowledge is very C/C++-specific and is useless in other languages because they don't have those classes of UB to begin with.

I dabbled in C++ programming for about 10 years before I discovered Rust. Once I wrote my first few Rust programs, I was hooked. Suddenly, I stopped worrying about all the stupid complexities and language minutiae of C++. Rust just made sense out of the box. It provided far fewer ways to do things ( https://www.nayuki.io/page/near-duplicate-features-of-cplusp... ), and the easy way is usually the safe and correct way.

To me, Rust is C++ done right. It has the expressive power and compactness of C++ but almost none of the downsides. It is the true intellectual successor to C++. C++ needs to hurry up and die already.