+1, especially loved the episode from a couple months back about using AI tools in development. Really got me thinking differently about the role of AI in a developer's workflow and how software development will evolve.
> eventually I came to the depressing conclusion that there’s no way to get a group of C experts — even if they are knowledgable, intelligent, and otherwise reasonable — to agree on the Friendly C dialect. There are just too many variations, each with its own set of performance tradeoffs, for consensus to be possible.
I think the only "C replacement" that is comparable in complexity to C is [Hare](https://harelang.org/), but several shortcomings make it unsuitable as an actual C replacement in many cases (little/no multithreading, no support for macOS/Windows, no LLVM or GCC support, etc.).
And why do you think Zig (and Odin, but I'm not really familiar with that one) is not comparable in complexity to C? If you start with C, replace the preprocessor language with the host language, replace undefined behavior with illegal behavior (panics in debug builds), add different pointer types for different types of pointers (single object pointers, many object pointers, fat many object pointers (slices), nullable pointers), and make a few syntactic changes (types go after the names of values in declarations, pointer dereference is a postfix operator, add defer to move expressions like deallocation to the end of the scope) and write a new standard library, you pretty much have Zig.
I think if you are going to fix C's footguns you'll have to change so much you end up with a totally new language anyway, and then why not be ambitious? It costs a lot to learn a new language and people aren't going to bother if the only benefit it brings is things that can sort of mostly be caught with compiler warnings and static analysis.
It's important to be careful here: a lot (most? all?) of these rejections are programs that could be sound in a hypothetical Rust variant that didn't assert the unique/"noalias" nature of &mut reference, but are in fact unsound in actual Rust.
I love Rust, but I after doing it for a little while, I completely understand the "brain drain" aspect... yes, I get significantly better programs, but it is tiring to fight the borrow-checker sometimes. Heck, I currently am procrastinating instead of going into the ring.
Anyhow, I won't go back to C++ land. Better this than whatever arcane, 1000-line, template-hell error message that kept me fed when I was there.
I've seen this concept tried a few times (For example, MS tried it with Managed C++). The inevitable problem you run into is any such language isn't C++. Because of that, you end up needing to ask, "why pick this unpopular half C/C++ implementation and not Rust/go/D/Java/python/common lisp/haskell."
A big hard to solve problem is you are likely using a C because of the ecosystem and/or the performance characteristics. Because of the C header/macro situation that becomes just a huge headache. All the sudden you can't bring in, say, boost because the header uses the quirks excluded from your smaller C language.
I still have a hard time adopting a language/ecosystem that was originally tied to a particular platform, and is still "owned" by the owners of that platform.
Sun actually did it right with Java, recognizing that if they mainly targeted SunOS/Solaris, no one would use it. And even though Oracle owns it now, it's not really feasible for them to make it proprietary.
Apple didn't care about other platforms (as usual) for quite a long time in Swift's history. Microsoft was for years actively hostile toward attempts to run .NET programs on platforms other than Windows. Regardless of Apple's or MS's current stance, I can't see myself ever bothering with Swift or C#/F#/etc. There are too many other great choices with broad platform and community support, that aren't closely tied to a corporation.
.NET recently had a (very) minor controversy for inserting
what amounts to a GitHub Copilot ad into their docs. So yeah, it sure feels like "once a corporate language, always a corporate language", even if it's transferred to a nominally independent org. It might not be entirely rational, but I certainly feel uncomfortable using Swift or .NET.
> Microsoft was for years actively hostile toward attempts to run .NET programs on platforms other than Windows
It's been 10 years. Even before that, no action was ever taken against Mono nor any restriction put or anything else. FWIW Swift shares a similar story, except Apple started to care only quite recently about it working anywhere else beyond their platforms.
> There are too many other great choices with broad platform and community support
:) No, thanks, I'm good. You know why I stayed in .NET land and didn't switch to, say, Go? It's not that it's so good, it's because most alternatives are so bad in one or another area (often many at the same time).
I too have been thinking a lot about a minimum viable improvement over C. This requires actually being able to incrementally port your code across:
* "No implicit type conversions" is trivial, and hardly worth mentioning. Trapping on both signed and unsigned overflow is viable but for hash-like code opting in to wrapping is important.
* "Safer strings" means completely different things to different people. Unfortunately, the need to support porting to the new language means there is little we can do by default, given the huge amount of existing code. We can however, add new string types that act relatively uniformly so that the code can be ported incrementally.
* For the particular case of arrays, remember that there are at least 3 different ways to compute its length (sentinel, size, end-pointer). All of these will need proper typing support. Particularly remember functions that take things like `(begin, middle end)`, or `(len, arr1[len], arr2[len])`.
* Support for nontrivial trailing array-or-other datums, and also other kinds of "multiple objects packed within a single allocation", is essential. Again, most attempted replacements fail badly.
* Unions, unfortunately, will require much fixing. Most only need a tag logic (or else replacement with bitcasting), but `sigval` and others like it are fundamentally global in nature.
* `va_list` is also essential to support since it is very widely used.
* The lack of proper C99 floating-point support, even in $CURRENTYEAR, means that compile-to-C implementations will not be able to support it properly either, even if the relevant operations are all properly defined in the new frontend to take an extra "rounding mode" argument. Note that the platform ABI matters here.
* There are quite a few things that macros are used for, but ultimately this probably is a finite set so should be possible to automatically convert with a SMOC.
Failure to provide a good porting story is the #1 mistake most new languages make.
I have a plan for a safe C and also type-safe generic and bounds-checked containers. Here is some experimental (!) example: https://godbolt.org/z/G4ncoYjfW
Except for some missing pieces, this is safe and I have a prototype based on GCC that would warn about any unsafe features. va_list can be safely used at least with format strings and for union I need an annotations. Life times are the bigger outstanding issue.
I mean things like: compilers don't support the pragmas, and if the compiler can "see" constants they are often evaluated with the wrong rounding mode.
I'm far from an expert but I've seen enough to know it's wrong.
Because it's easier to add a warning or error. Don't like implicit conversions? Add a compiler flag, and the issue is basically gone.
Safer strings is harder, as it gets into the general memory safety problem, but people have tried adding safer variants of all the classic functions, and warnings around them.
I have a plan for a safe subset of C which would just require a compiler to warn about certain constructs. I also have a proposal for a safe string type. I am not so sure about type conversions though, you get useful warnings already with existing compiler flags and you can solve the problem in the article already just by wrapping the types in structs.
Yes, Rust is better. Implicit numeric conversion is terrible. However, don't use atoi if you're writing C++ :-). The STL has conversion functions that will throw, so separate problem.
It's bad if it alters values (e.g. rounding). Promotion from one number representation to another (as long as it preserves values) isn't bad. This is trickier than it might seem, but Virgil has a good take on this (https://github.com/titzer/virgil/blob/master/doc/tutorial/Nu...). Essentially, it only implicitly promotes values in ways that don't lose numeric information and thus are always reversible.
In the example, Virgil won't let you pass "1000.00" to an integer argument, but will let you pass "100" to the double argument.
Aside from the obvious bit size changes (e.g. i8 -> i16 -> i32 -> i64, or f32 -> f64), there is no "hierarchy" of types. Not all ints are representable as floats. u64 can represent up to 2^64 - 1, but f64 can only represent up to 2^53 with integer-level precision. This issue may be subtle, but Rust is all about preventing subtle footguns, so it does not let you automatically "promote" integers to float - you must be explicit (though usually all you need is an `as f64` to convert).
> Aside from the obvious bit size changes (e.g. i8 -> i16 -> i32 -> i64, or f32 -> f64), there is no "hierarchy" of types.
Depends on what you want from such a hierarchy, of course, but there is for example an injection i32 -> f64 (and if you consider the i32 operations to be undefined on overflow, then it’s also a homomorphism wrt addition and multiplication). For a more general view, various Schemes’ takes on the “numeric tower” are informative.
Virgil allows the maximum amount of implicit int->float injections that don't change values and allows casts (in both directions) that check if rounding altered a value. It thus guarantees that promotions and (successful) casts can't alter program behavior. Given any number in representation R, promotion or casting to type N and then casting back to R will return the same value. Even for NaNs with payloads (which can happen with float <-> double).
I disagree: when you use floats, you implicitly accept the precision loss/roundings that comes with using floats..
IMHO int to float implicit conversion is fine as long as you have explicit float to int conversion.
The numeric conversion functions in the STL are terrible. They will happily accept strings with non-numeric characters in them: they will convert "123abc" to 123 without giving an error. The std::sto* functions will also ignore leading whitespace.
Yes, you can ask the std::sto* functions for the position where they stopped because of invalid characters and see if that position is the end of the string, but that is much more complex than should be needed for something like that.
These functions don't convert a string to a number, they try to extract a number from a string. I would argue that most of the time, that's not what you want. Or at least, most of the time it's not what I need.
atoi has the same problem of course, but even worse.
std::from_chars will still accept "123abc". You have to manually check if all parts of the string have been consumed. On the other hand, " 123" is not accepted, because it starts with an invalid character, so the behaviour isn't "take the first acceptable number and parse that" either.
To get the equivalent of Rust's
if let Ok(x) = input.parse::<i32>() {
println!("You entered {x}");
} else {
eprintln!("You did not enter a number");
}
you need something like:
int x{};
auto [ptr, ec] = std::from_chars(input.data(), input.data() + input.size(), x);
if (ec == std::errc() && ptr == input.data() + input.size()) {
std::cout << "You entered " << x << std::endl;
} else {
std::cerr << "You did not enter a valid number" << std::endl;
}
I find the choice to always require a start and and end position, and not to provide a method that simply passes or fails, to be quite baffling. In C++26, they also added an automatic boolean conversion for from_chars' return type to indicate success, which considers "only consumed half the input from the start" to be a success.
Maybe I'm weird for mostly writing code that does straightforward input-to-number conversions and not partial string parsers, but I have yet to see a good alternative for Rust's parse().
I guess there's a place for functions that extract or parse partially, but IMO there is a real need for an actual conversion function like Rust's parse() or Python's int() or float(). I think it's a real shame C++ (and C as well) only offers the first and not the second.
If the conversion will always succeed (for example an 8-bit unsigned integer to a 32-bit unsigned integer), the From trait would be used to allow the conversion to feel implicit.
If the conversion could fail (for example a 32-bit unsigned integer to an 8-bit unsigned integer), the TryFrom trait would be used so that an appropriate error could be returned in the Result.
These traits prevent errors when converting between types and clearly mark conversions that might fail since they return Result instead of the output type.
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".
unfortunately it's not so simple. that's the convention. depending on the library you're using it might be a special type of Error, or special type of Result, something needs to be transformed, `?` might not work in that case (unless you transform/map it), etc.
I like rust, but its not as clean in practice, as you describe
Your owned (i.e. not third-party) Error type is a sum type of error types that might be thrown by other libraries, with a newtype wrapper (`IOError`) on top.
Then implement the `From` trait to map errors from third-party libraries to your own custom Error space:
Errors are where I find zig severely lacking. They can't carry context. Like if you're parsing a JSON file and it fails, you can know that it failed but not where it failed within the file. Their solution in the standard library for cases like this was to handle printing to stderr internally, but that is incredibly hacky.
The std has diagnostics struct you can give to the json parser. Zig is manually memory managed language so it doesnt have payloads in errors for a good reason.
Manual memory management is not a reason Zig couldn't have supported sum types for error returns. You don't need an allocator involved at all to return a discriminated union like Rust uses.
I actually find Zig quite a pleasant language other than my gripe with not handling more complex errors as cleanly.
You would have to give up on global error set with sum types, unless you want to see the global error set bloat with the largest sum type. Also if the error payload needs allocating what are you gonna do? Allocating is no no no here. I know rust will just panic in OOM but zig chooses not to do that. Even if you allocated, since zig has no borrow checker the error handling becomes nightmare as you now have to deal with freeing the potential allocations.
Using out parameters for context, or diagnostic pattern like std does is not bad at all imo.
The only nice thing error payloads give you is better error messages in case of uncaught errors.
Maybe contrarian, but imo the `Result` type, while kind of nice, still suffers from plenty of annoyances, including sometimes not working with the (manpages-approved) `dyn Error`, sometimes having to `into()` weird library errors that don't propagate properly, or worse: `map_err()` them; I mean, at this point, the `anyhow` crate is basically mandatory from an ergonomics standpoint in every Rust project I start. Also, `?` doesn't work in closures, etc.
So, while this is an improvement over C++ (and that is not saying much at all), it's still implemented in a pretty clumsy way.
? definitely works in closures, but it often takes a little finagling to get working, like specifying the return type of the closure or setting the return type of a collect to a Result<Vec<_>>
Mapping a Vec<T> to Result<U, E> and collecting them into a single Result<Vec<U>, E> made me feel like a ninja when I first learned it was supported. I’m a little worried it’s too confusing to read for others, but it works so well.
Combined with futures::try_join_all for async closures and you can use it to do a bunch of failable tasks in parallel too, it’s great.
> the `anyhow` crate is basically mandatory from an ergonomics standpoint in every Rust project I start
If you use `anyhow`, then all you know is that the function may `Err`, but you do not know how - this is no better than calling a function that may `throw` any kind of `Throwable`. Not saying it's bad, it is just not that much different from the error handling in Kotlin or C#.
+1 for snafu. It lets you blend anyhow style errors for application code with precise errors for library code. .context/.with_context is also a lovely way to propagate errors between different Result types.
I know a ‘C’ code base that treats all socket errors the same and just retries for a limited time. However there are errors that make no sense to retry, like invalid socket or socket not connected. It is necessary to know what socket error occurred. I like how the Posix API defines an errno and documents the values. Of course this depends on accurate documentation.
This is an IDE/documentation problem in a lot of cases though. No one writes code badly intentionally, but we are time constrained - tracking down every type of error which can happen and what it means is time consuming and you're likely to get it wrong.
Whereas going with "I probably want to retry a few times" is guessing that most of your problems are the common case, but you're not entirely sure the platform you're on will emit non-commoncases with sane semantics.
Better than C, sufficient in most cases if you're writing an app, to be avoided if you're writing a lib. There are alternatives such as `snafu` or `thiserror` that are better if you need to actually catch the error.
I find myself working through a hierarchy of error handling maturity as a project matures.
Initial proof of concepts just get panics (usually with a message).
Then functions start to be fallible, by adding anyhow & considering all errors to still be fatal, but at least nicely report backtraces (or other things! context doesn't have to just be a message)
Then if a project is around long enough, swap anyhow to thiserror to express what failure modes a function has.
There's some space for improvement, but really... not a lot? Result is a pretty basic type, sure, but needing to choose a dependency to get a nicer abstraction is not generally considered a problem for Rust. The stdlib is not really batteries included.
Doing error handling properly is hard, but it's a lot harder when error types lose information (integer/bool returns) or you can't really tell what errors you might get (exceptions, except for checked exceptions which have their own issues).
Sometimes error handling comes down to "tell the user", where all that info is not ideal. It's too verbose, and that's when you need anyhow.
In other cases where you need details, anyhow is terrible. Instead you want something like thiserror, or just roll your own error type. Then you keep a lot more information, which might allow for better handling. (HttpError or IoError - try a different server? ParseError - maybe a different parse format? etc.)
So I'm not sure it's that Result is clumsy, so much that there are a lot of ways to handle errors. So you have to pick a library to match your use case. That seems acceptable to me?
FWIW, errors not propagating via `?` is entirely a problem on the error type being propagated to. And `?` in closures does work, occasionally with some type annotating required.
I agree with you, but it’s definitely inconvenient. Result also doesn’t capture a stack trace. I spent a long time tracking down bugs in some custom binary parsing code awhile ago because I had no idea which stack trace my Result::Err’s were coming from. I could have switched to another library - but I didn’t want to inflict extra dependencies on people using my crate.
As you say, it’s not “batteries included”. I think that’s a fine answer given rust is a systems language. But in application code I want batteries to be included. I don’t want to need to opt in to the right 3rd party library.
I think rust could learn a thing or two from Swift here. Swift’s equivalent is better thought through. Result is more part of the language, and less just bolted on:
Generally, I agree the situation with errors is much better in Rust in the ways you describe. But, there are also panics which you can catch_unwind[1], set_hook[2] for, define a #[panic_handler][3] for, etc.
Yeah, in anything but heavily multi-threaded servers, it's usually best to immediately crash on a panic. Panics don't mean "a normal error occurred", they mean, "This program is cursed and our fundamental assumptions are wrong." So it's normal for a unit test harness to catch panics. And you may occasionally catch them and kill an entire client connection, sort of the way Erlang handles major failures. But most programs should just exit immediately.
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.
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.
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:
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.
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).
There does not need to be an additional cost for this.
Most users will do this:
1. Check if there is a value
2. Get the value
There is nothing theoretically preventing the compiler from enforcing that step 1 happens before step 2, especially if the compiler is able to combine the control flow branch with the process of conditionally getting the value. The practical issue is that there's no way to express this in C++ at all. The best you can do is the visitor pattern, which has horrible ergonomics and you can only hope it doesn't cause worse code generation too.
Some users want to do this:
1. Grab the value without checking to see if it's valid. They are sure it will be valid and can't or don't want to eat the cost of checking.
There is nothing theoretically preventing this from existing as a separate method.
I'm not a rust fanboy (seriously, check my GitHub @jchv and look at how much Rust I write, it's approximately zero) but Rust has this solved six ways through Sunday. It can do both of these cases just fine. The only caveat is that you have to wrap the latter case in an unsafe, but either way, you're not eating any costs you don't want to.
C++ can do this too. C++ has an active proposal for a feature that can fix this problem and make much more ergonomic std::variant possible, too.
Of course, this is one single microcosm in the storied history of C++ failing to adequately address the problem of undefined behavior proliferating the language, so I don't have high hopes.
I might have misinterpreted what you mean as “standard” (I was originally thinking like you meant that the standard dictates all operations must be safe and doing things like the unsafe/unchecked option references would be outside the standard). I realized that you might mean it like it should be the default behavior, so correct me if I’m wrong there.
Yeah I agree that sane defaults would be nice and that things like unchecked accesses should be more explicit. Having linear types to properly enforce issues with things like use-after-move would also be awesome. I don’t know that anyone has ever accused C++ of being particularly ergonomic. Sum types and pattern matching would be awesome.
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).
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 ...
I cannot follow your rant... I'll do my best to respond, but I'm probably not understanding something.
Divide by zero must be undefined behavior in any performant language. On x86 you either have a if before running the divide (which of course in some cases the compiler can optimize out, but only if it can determine the value is not zero); or you the CPU will trap into the OS - different OSes handle this in different ways, but most not in a while that makes it possible to figure out where you were and thus do something about it. This just came up in the C++ std-proposals mailing list in the past couple weeks.
AFAIK all common CPUs have the same behavior on integer overflow (two-complement). However in almost all cases (again, some encryption code is an exception) that behavior is useless to real code and so if it happens your code has a bug either way. Thus we may as well let compilers optimize assuming it cannot happen as it if it does you have a bug no matter what we define it as. (C++ is used on CPUs that are not two-complement as well, but we could call this implementation defined or unspecified, but it doesn't change that you have a bug if you invoke it.)
For std::expected - new benchmarks are proving in the real world, and with optimized exception handlers that exceptions are faster in the real world than systems that use things like expected. Microbenchmarks that show exceptions are slower are easy to create, but real world exceptions that unwind more than a couple function calls show different results.
As for modules, support is finally here and early adopters are using it. The road was long, but it is finally proving it worked.
Long roads are a good thing. C++ has avoided a lot of bad designs by spending a lot of time thinking about problems about things for a long time. Details often matter and move fast languages tend to run into problems when something doesn't work as well as they want. I'm glad C++ standardization is slow - it already is a mess without add more half backed features to the language.
> Divide by zero must be undefined behavior in any performant language. On x86 you either have a if before running the divide (which of course in some cases the compiler can optimize out, but only if it can determine the value is not zero); or you the CPU will trap into the OS - different OSes handle this in different ways, but most not in a while that makes it possible to figure out where you were and thus do something about it. This just came up in the C++ std-proposals mailing list in the past couple weeks.
I mean look, I already agree that it's not necessarily unreasonable to have undefined behavior, but this statement is purely false. You absolutely can eat your cake and have it too. Here's how:
- Split the operation in two: safe, checked division, and fast, unchecked division.
- OR, Stronger typing; a "not-zero" type that represents a numeric type where you can guarantee the value isn't zero. If you can't eat the cost of runtime checks, you can unsafely cast to this.
I think the former is a good fit for C++.
C++ does not have to do what Rust does, but for sake of argument, let's talk about it. What Rust does here is simple, it just defines divide-by-zero to panic. How? Multiple ways:
- If it knows statically it will panic, that's a compilation error.
- If it knows statically it can not be zero, it generates unchecked division.
- If it does not know statically, it generates a branch. (Though it is free to implement this however it wants; could be done using CPU exceptions/traps if they wanted.)
What if you really do need "unsafe" division? Well, that is possible, with unchecked_div. Most people do not need unchecked_div. If you think you do but you haven't benchmarked yet, you do not. It doesn't get any simpler than that. This is especially the case if you're working on modern CPUs with massive pipelines and branch predictors; a lot of these checks wind up having a very close to zero cost.
> AFAIK all common CPUs have the same behavior on integer overflow (two-complement). However in almost all cases (again, some encryption code is an exception) that behavior is useless to real code and so if it happens your code has a bug either way. Thus we may as well let compilers optimize assuming it cannot happen as it if it does you have a bug no matter what we define it as. (C++ is used on CPUs that are not two-complement as well, but we could call this implementation defined or unspecified, but it doesn't change that you have a bug if you invoke it.)
It would be better to just do checked arithmetic by default; the compiler can often statically eliminate the checks, you can opt out of them if you need performance and know what you're doing, and the cost of checks is unlikely to be noticed on modern processors.
It doesn't matter that this usually isn't a problem. It only has to be a problem once to cause a serious CVE. (Spoiler alert: it has happened more than once.)
> For std::expected - new benchmarks are proving in the real world, and with optimized exception handlers that exceptions are faster in the real world than systems that use things like expected. Microbenchmarks that show exceptions are slower are easy to create, but real world exceptions that unwind more than a couple function calls show different results.
You can always use stack unwinding or exceptions if you want to; that's also present in Rust too, in the form of panic. The nice thing about something like std::expected is that it theoretically can bridge the gap between code that uses exceptions and code that doesn't: you can catch an exception and stuff it into the `e` of an std::expected value, or you can take the `e` value of an std::expected and throw it. In theory this should not have much higher cost than simply throwing.
> As for modules, support is finally here and early adopters are using it. The road was long, but it is finally proving it worked.
Google has a odd C++ style guide that rules out a lot of useful things for their own reasons.
There is no reason why make could not work with modules if someone wanted to go through the effort. The CMake people have even outlined what needs to be done. Ninja is so much nicer that you should switch anyway - I did more than 10 years ago.
I do use Ninja when I use CMake, but honestly that mostly comes down to the fact that the Makefiles generated by CMake are horrifically slow. I don't particularly love CMake, I only use it because the C++ ecosystem really has nothing better to offer. (And there's no chance I'm going to redistribute a project that can't build with the Makefile generator, at least unless and until Ninja is default.)
Anyway, the Google C++ style guide has nothing to do with why C++ modules aren't and won't be used at Google, it's because as-implemented modules are not an obvious win. They can theoretically improve performance, but they can and do also make some cases worse than before.
I don't think most organizations will adopt modules at this rate. I suspect the early adopters will wind up being the only adopters for this one.
> What if you really do need "unsafe" division? Well, that is possible, with unchecked_div. Most people do not need unchecked_div. If you think you do but you haven't benchmarked yet, you do not. It doesn't get any simpler than that.
This attitude is why modern software is dogshit slow. People make this "if you haven't benchmarked, it doesn't matter" argument thousands of times and the result is that every program I run is slower than molasses in Siberia. I don't care about "safety" at the expense of performance.
> This attitude is why modern software is dogshit slow.
Bullshit. Here's my proof: We don't even do this. There's a ton of software that isn't safe from undefined behavior and it's still slow as shit.
> People make this "if you haven't benchmarked, it doesn't matter" argument thousands of times and the result is that every program I run is slower than molasses in Siberia. I don't care about "safety" at the expense of performance.
If you can't imagine a world where there's nuance between "we should occasionally eat 0.5-1ns on checking an unsafe division" and "We should ship an entire web browser with every text editor and chat app" the problem is with you. If you want your software to be fast, it has to be benchmarked, just like if you want it to be stable, it has to be tested. There's really no exceptions here, you can't just guess these things.
Modern software is dogshit slow because it's a pile of JS dependencies twenty layers deep.
Checked arithmetic is plenty fast. And as for safety vs performance, quickly computing the incorrect result is strictly less useful than slowly computing the correct one.
The problem is 'undefined behaviour' is far too powerful.
Why not make division by zero implementation defined. I'm happy with my compiler telling me my program will get terminated if I divide by zero, no problem. Let's even say it "may" be terminated (because maybe the division is optimised out if we don't actually need to calculate it, fine).
My problem is that UB let's compilers do all kinds of weird things, like assume if I write:
Then set dividebyzero to always be 0, because 'obviously' y can't be 0, because then I would invoke undefined behaviour.
Also, for two-complement, it's fairly common people want wrapping behaviour. Also, I don't think basically anyone is using C++ on non-twos complement CPUs (both gcc and clang don't support it), and even if it does run on such CPUs, why not still require a well-defined behaviour, in the same way C++ runs on 32-bit and 64-bit systems, but we don't say asking for the size of a pointer is undefined behaviour -- everyone just defines what it is on their system!
> 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.
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
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.
`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.
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.
> 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").
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?
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.
I'm not sure who in your mind is forgetting that, or what the rest of your comment means to communicate.
Who are you speaking to who hasn't explored all those things in depth?
I see Rust's restrictions as a huge advantage over C++ here. Even with respect to editions. Rust has always given me the impression of a language designed from the start to be approximately what C++ is today, without the cruft, in which safety is opt-out, not opt-in. And the restrictions seem more likely to preserve that than not.
C/C++ folks seem to see Rust's restrictions as anti-features without realizing that C/C++'s lack of restriction resulted in the situation they have today.
I only maintain a few projects in each language, so I haven't run into every sort of issue for either, but that's very much how it feels to me still, several years and several projects in.
Many of the members of the Rust Evangelism Strike Force, as main audience. That is to whom it is targeted for, given the usual kind of content that some write about.
I agree that Rust is designed to be like C++ is today, without the cruft, except all languages if they survive long enough in the market, beyond the adoption curve, they will eventually get their own cruft.
Not realizing this, will only make that 30 years from now, if current languages haven't yet been fully replaced by AI based tools, there will be that language designed to be like Rust is in 30 years, but without the cruft.
The strength of C++ code today is on the ecosystem, that is why we reach for it, having to write CUDA, DirectX, maybe dive into the innards of Java, CLR, V8, GCC, LLVM, doing HPC with OpenAAC, OpenMP, MPI, Metal, Unreal, Godot, Unity.
Likewise I don't reach for C for fun, the less the merrier, rather POSIX, OpenGL, Vulkan,....
> Many of the members of the Rust Evangelism Strike Force, as main audience.
Well I'm not them. I'm just a regular old software developer.
> The strength of C++ code today is on the ecosystem
Ecosystem is why I jumped ship from C++ to Rust. The difference in difficulty integrating a random library into my project is night and day. What might take a week or a month in C++ (integrating disparate build systems, establishing types and lifetimes of library objects and function calls, etc) takes me 20 minutes in Rust. And in general I find the libraries to be much smaller, more modular, and easier to consume piecemeal rather than a whole BOOST or QT at a time.
And while the Rust libraries are younger, I find them to be more stable, and often more featureful and with better code coverage. The language seems to lend itself to completionism.
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.
Meaning that all the machines I've ever cared about have had 8 bit bytes. The TI-99/4A, TRS-80, Commodore 64 and 128, Tandy 1000 8088, Apple ][, Macintosh Classic, etc.
Many were launched in the late 70s. By 1985 we were well into the era of PC compatibles.
in 1985 PC compatibles were talked about, but systems like VAX, and mainframes were still very common and considered the real computers while PCs were toys for executives. PCs had already shown enough value (via word processors and spreadsheets) that everyone knew they were not going away. PCs lacked things like multi-tasking that even then "real" computers had for decades.
My https://en.wikipedia.org/wiki/Tandy_1000 came out in 1984. And it was a relatively late entry to the market, it was near peak 8088 with what was considered high end graphics and sound for the day, far better than the IBM PC which debuted in 1981 and only lasted until 1987.
Yeah, I almost called that out. Probably should have. GPU/NPU feels new (at least for us folks who could never afford a Cray). Probably the biggest change in the last 20 years, especially if you classify it with other multi-core development.
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.
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.
> 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
This isn't really true since Rust has panics. It would be nice to have out-of-the-box support for a "no panics" subset of Rust, which would also make it easier to properly support linear (no auto-drop) types.
The problem is with false positives. Even if you clearly see that some function will never panic (but it uses some feature which may panic), compiler might not always see that. If compiler says that there are no panics, then there are no panics, but is it enough to add as part of the language if you need to mostly avoid using features that might panic?
That's going to be difficult because the language itself requires panic support to properly implement indexing, slicing, and integer division. There are checked methods that can be used instead, but to truly eliminate panics, the ordinary operators would have to be banned when used with non-const arguments, and this restriction would have to propagate to all dependencies as well.
Yes that’s right. The feature really wants compiler support for that reason. The simplest version wouldn’t be too hard to implement. Every function just exports a flag on whether or not it (or any callees) can panic. Then we have a nopanic keyword which emits a compiler error if the function (or any callee) panics.
It would be annoying to use - as you say, you couldn’t even add regular numbers together or index into an array in nopanic code. But there are ways to work around it (like the wrapping types).
One problem is that implicit nopanic would add a new way to break semver compatibility in APIs. Eg, imagine a public api that just happens to not be able to panic. If the code is changed subtly, it could easily start panicing again. That could break callers, so it has to be a major version bump. You’d probably have to require explicit nopanic at api boundaries. (Else assume all public functions from other crates can panic). And because of that, public APIs like std would need to be plastered with nopanic markers everywhere. It’s also not clear how that works through trait impls.
As far as I can tell, no_std doesn't change anything with regard to either the usability of panicking operators like integer division, slice indexing, etc. (they're still usable) nor on whether they panic on invalid input (they still do).
You can handle panics. It’s for unrecoverable errors, but internally it does stack unwinding by default like exceptions in C++.
You see this whenever you use cargo test. If a single test panics, it doesn’t abort the whole program. The panic is “caught”. It still runs all the other tests and reports the failure.
> but internally it does stack unwinding by default
Although as a library vendor, you kind have to assume your library could be compiled into an app configured with panic=abort, in which case it will not do that
I wish more people (and crate authors) would treat panic!() as it really should be treated: only for absolutely unrecoverable errors that indicate that some sort of state is corrupted and that continuing wouldn't be safe from a data- or program-integrity perspective.
Even then, though, I do see a need to catch panics in some situations: if I'm writing some sort of API or web service, and there's some inconsistency in a particular request (even if it's because of a bug I've written), I probably really would prefer only that request to abort, not for the entire process to be torn down, terminating any other in-flight requests that might be just fine.
But otherwise, you really should just not be catching panics at all.
would you consider panics acceptable when you think it cannot panic in practice?
e.g. unwraping/expecting a value for a key in a map when you inserted that value before and know it hasn't been removed?
you could have a panic though, if you wrongly make assumptions
I don't speak for anyone else but I'm not using `unwrap` and `expect`. I understand the scenario you outlined but I've accepted it as a compromise and will `match` on a map's fetching function and will have an `Err` branch.
I will fight against program aborts as hard as I possibly can. I don't mind boilerplate to be the price paid and will provide detailed error messages even in such obscure error branches.
Again, speaking only for myself. My philosophy is: the program is no good for me dead.
Agreed, that's why I don't catch panics either -- if we get to that point I'm viewing the program as corrupted. I'm simply saying that I do my utmost to never use potentially panicking Rust API and prefer to add boilerplate for `Err` branching.
This implies that every function in your library that ever has to do anything that might error out - e.g. integer arithmetic or array indexing - has to be declared as returning the corresponding Result to propagate the error. Which means that you are now imposing this requirement (to check for internal logic bugs in library code) onto the user of your library.
Well, I don't write as huge a code as this though, nor does it have as many layers.
Usually I just use the `?` and `.map_err` (or `anyhow` / `thiserror`) to delegate and move on with life.
I have a few places where I do pattern-matches to avoid exactly what you described: imposing the extra internal complexity to users. Which is indeed a bad thing and I am trying to fight it. Not always succeeding.
Obviously yes. For the same reason it's acceptable that myvec[i] panics (it will panic if i is out of bounds - but you already figured out that i is in bounds) and a / b panic for a and b integers (it will panic if b is zero, but if your code is not buggy you already tested if b is zero prior to dividing right?)
Panic is absolutely fine for bugs, and it's indeed what should happen when code is buggy. That's because buggy code can make absolutely no guarantees on whether it is okay to continue (arbitrary data structures may be corrupted for instance)
Indeed it's hard to "treat an error" when the error means code is buggy. Because you can rarely do anything meaningful about that.
This is of course a problem for code that can't be interrupted.. which include the Linux kernel (they note the bug, but continue anyway) and embedded systems.
Note that if panic=unwind you have the opportunity to catch the panic. This is usually done by systems that process multiple unrelated requests in the same program: in this case it's okay if only one such request will be aborted (in HTTP, it would return a 5xx error), provided you manually verify that no data structure shared by requests would possibly get corrupted. If you do one thread per request, Rust does this automatically; if you have a smaller threadpool with an async runtime, then the runtime need to catch panics for this to work.
> Note that if panic=unwind you have the opportunity to catch the panic.
And now your language has exceptions - which break control flow and make reasoning about a program very difficult - and hard to optimize for a compiler.
Yeah, but this isn't the only bad thing about unwinding. Much worse than just catching panics is the fact that a panic in a thread takes down only that thread (except if it is in the main thread). If your program is multithreaded, panic=unwind makes it much harder to understand how it reacts to errors, unless you take measures to shut down the program if any thread panic (which again, requires catch_unwind if you have unwinding). Also: that's why locks in Rust have poisoning, they exist so that panics propagate between threads: if a thread panics while holding a lock, any other thread attempting to acquire this lock will panic too (which is better than a deadlock for sure)
And that's why my programs get compiled with panic=abort, that makes panics just quit the program, with no ability to catch them, and no programs in zombie states where some threads panicked and others keep going on.
But see, catch_panic is an escape hatch. It's not meant to be used as a general error handling mechanism and even when doing FFI, Rust code typically converts exceptions in other languages into Results (at a performance cost, but who cares). But Rust needs a escape right, it is a low level language.
And there is at least one case where the catch_unwind is fully warranted: when you have an async web server with multiple concurrent requests and you need panics to take down only a single request, and not the whole server (that would be a DoS vector). If that weren't possible, then async Rust couldn't have feature parity with sync Rust (which uses a thread-per-request model, and where panics kill the thread corresponding to the request)
Unfortunately even the Rust core language doesn't treat them this way.
I think it's arguably the single biggest design mistake in the Rust language. It prevents a ton of useful stuff like temporarily moving out of mutable references.
They've done a shockingly good job with the language overall, but this is definitely a wart.
Using a Rust lib from Swift on macOS I definitely want to catch panics - to access security scoped resources in Rust I need the Rust code to execute in process (I believe) but I’d also like it not to crash the entire app.
> I probably really would prefer only that request to abort, not for the entire process to be torn down,
This is a sign you are writing an operating system instead of using one. Your web server should be handling requests from a pool of processes - so that you get real memory isolation and can crash when there is a problem.
Even if you used a pool of processes, that's still not one process per request, and you still don't want one request crashing to tear down unrelated requests.
I question both things. I would first of all handle each request in its own process.
If there was a special case that would not work, then the design dictates that requests are not independent and there must be risk of interference (they are in the same process!)
What I definitely do not want is a bug ridden “crashable async sub task” system built in my web program.
This is simply a wrong idea about how to write web servers. You're giving up scalability massively, only to gain a minor amount of safety - one that is virtually irrelevant in a memory safe language, which you should anyway use. The overhead of process-per-request, or even thread-per-request, is absurd if you're already using a memory safe language.
you’re vastly over estimating the overhead of processes and number of simultaneous web connections.
> only to gain a minor amount of safety
What you’re telling me is performance (memory?) is such a high priority you’re willing to make correctness and security tradeoffs.
And I’m saying thats ok, one of those is crashing might bring down more than one request.
> one that is virtually irrelevant in a memory safe language
Your memory safe language uses C libraries in its process.
Memory safe languages have bugs all the time. The attack surface is every line of your program and runtime.
Memory is only one kind of resource and privilege. Process isolation is key for managing resource access - for example file descriptors.
Chrome is a case study if these principles. Everybody thought isolating JS and HTML pages should be easy - nobody could get it right and chrome instead wrapped each page in a process.
> you’re vastly over estimating the overhead of processes and number of simultaneous web connections.
It's less the actual overhead of the process but the savings you get from sharing. You can reuse database connections, have in-memory caches, in-memory rate limits and various other things. You can use shared memory which is very difficult to manage or an additional common process, but either way you are effectively back to square one with regards to shared state that can be corrupted.
Please find one web server being actively developed using one process per request.
Handling thousands of concurrent requests is table stakes for a simple web server. Handling thousands of concurrent processes is beyond most OSs. The context switching overhead alone would consume much of the CPU of the system. Even hundreds of processes will mean a good fraction of the CPU being spent solely on context switching - which is a terrible place to be.
> Handling thousands of concurrent processes is beyond most OS
It works fine on Linux - the operating system for the internet. Have you tried it?
> good fraction of the CPU being spent solely on context switching
I was waiting for this one. Threads and processes do the same amount of context switching. The overhead of processes switch is a little higher. The main cost is memory.
> Threads and processes do the same amount of context switching.
Yes, therefore real webservers use a limited amount of threads/processes (in the same ballpark as a number of CPU cores). Modern approach is to use green threads which are really cheap to switch, it is like store registers, read registers and jmp.
> The main cost is memory.
The main cost is scheduling, not switching per se. Preemptive multitasking needs to deal with priorities to not waste time, and algorithms that do it are O(N) mostly. All these O(N) calculations needs to be completed multiple times per second, the higher the frequency of switching the more work to do. When you have thousands of processes it is the main cost. If you have tens of thousands it starts to bite hard.
> The main cost is scheduling, not switching per se. Preemptive multitasking needs to deal with priorities to not waste time, and algorithms that do it
The person I am having a conversation with is advocating for threads instead of processes. How do you think threads work?
> Modern approach is to use green threads which are really cheap to switch, it is like store registers, read registers and jmp.
That’s certainly the popular approach. As I said at the beginning this approach is making a mini operating system with more bugs and less security rather than leveraging the capabilities of your operating system.
Once again, im waiting to here about your experience of maxing out processes and after that having to switch to green threads.
> The person I am having a conversation with is advocating for threads instead of processes. How do you think threads work?
Are they? I looked back and I've found this quote of them: "The overhead of process-per-request, or even thread-per-request, is absurd if you're already using a memory safe language." Doesn't seem as an advocacy for thread-per-request to me.
> As I said at the beginning this approach is making a mini operating system with more bugs and less security rather than leveraging the capabilities of your operating system.
Lets look at Apache for example. It starts a few processes and/or threads, but then each thread deals with a lot of connections. The threads Apache starts are for spreading work over several CPUs and maybe to overcome some limits of select/poll/epoll. The main approach is to track a state of a connection, and when something happens on a socket, Apache find the state of the connection and deals with events on the socket. Then it stores the new state and moves to deal with other sockets in the same manner.
It is like green threads but without green threads. Green threads streamlines all this state keeping by allowing each connection to have it's own stack. And I'd say it is easier to do right than to write a finite automata for HTTP/HTTPS.
> Once again, im waiting to here about your experience of maxing out processes and after that having to switch to green threads.
Oh, I didn't. A long long time ago I was reading stuff on networking. All of it was in one opinion: 10k kernel tasks maybe a tolerable solution, but 100k is bad. IIRC Apache had a document describing its internal architecture and explaining why it is as it is.
So I wouldn't even try to start thousands of threads. I mean I tried to start 1000s of processes when I was young and learned about fork-bombs, and this experience confirmed it for me, that 1000s of processes is not a really good idea.
Moreover I completely agree with them: if you use a memory-safe language, then it is strange to pay costs for preemptive multitasking just to have separate virtual address spaces. I mean, it will be better to get a virtual machine with JIT compiler, and run code for different connection on different instances of a virtual machine. O(1) complexity of cooperative switching will beat O(N) complexity of preemptive switching. To my mind hardware memory management is overrated.
Apache has years of engineering work - and almost weekly patches to fix issues related to security. Many of these security issues would go away if they were not using special technique to optimize performance.
But the best part of the web is its modular. So now your application doesn’t need to that. It can leverage those benefits without complexity cascade.
For example, Apache can manage more connections than your application needs running processes for.
> I was reading stuff on networking….
That’s exactly my point. Too many people are repeating advice from Google or Facebook and not actually thinking about real problems they face.
Can you serve more requests using specialized task management? Yes. You can make a mini-OS with fewer features to squeeze out more scheduling performance and that’s what some big companies did.
But you will pay for that with reduced security and reliability. To bring it back to my original complaint - you must accept that a crash can bring down multiple requests.
And it’s an insane default to design Rust around. It’s especially confusing to make all these arguments about how “unsafe” languages are, but then ignore OS safety in hopes of squeezing out a little more perf.
> So I wouldn't even try to start thousands of threads.
Please try it before arguing it doesn’t work. Fork bombing is recursive and unrelated.
> if you use a memory-safe language, then it is strange to pay costs for preemptive multitasking just to have separate virtual address spaces
Then why do these “memory-safe” languages need constant security patches? Why does chrome need to wrap each page’s JS in its own process?
In theory you’re right. If they are actually memory-safe then you don’t need to consider address spaces. But in practice the attack surface is massive and processes give you stronger invariants.
> The person I am having a conversation with is advocating for threads instead of processes. How do you think threads work?
I was certainly not, I explicitly said that thread-per-request is as bad as process-per-request. I could even agree that it's the worse of both worlds to some extent - none of the isolation, almost all of the overhead (except if you're using a language with a heavy runtime, like Java, where spawning a new JVM has a huge cost compared to a new thread in an existing JVM).
Modern operating systems provide many mechanisms for doing async IO specifically to prevent the need for spawning and switching between thousands of processes. Linux in particular has invested heavily in this, from select, to poll, to epoll, and now unto io_uring.
OS process schedulers are really a poor tool for doing massively parallel IO. They are a general purpose algorithm that has to keep in mind many possible types of heterogeneous processes, and has no insight into the plausible behaviors of those. For a constrained problem like parallel IO, it's a much better idea to use a purpose-built algorithm and tool. And they have simply not been optimized with this kind of scale in mind, because it's much more important and common use case to run quickly for a small number of processes than it is to scale up to thousands. There's a reason typical ulimit configurations are limited to around 1000 threads/processes per system for all common distros.
Honestly, I don't think libraries should ever panic. Just return an UnspecifiedError with some sort of string. I work daily with rust, but I wish no_std and an arbitrary no_panic would have better support.
Example docs for `foo() -> Result<(), UnspecifiedError>`:
# Errors
`foo` returns an error called `UnspecifiedError`, but this only
happens when an anticipated bug in the implementation occurs. Since
there are no known such bugs, this API never returns an error. If
an error is ever returned, then that is proof that there is a bug
in the implementation. This error should be rendered differently
to end users to make it clear they've hit a bug and not just a
normal error condition.
Imagine if I designed `regex`'s API like this. What a shit show that would be.
If you want a less flippant take down of this idea and a more complete description of my position, please see: https://burntsushi.net/unwrap/
> Honestly, I don't think libraries should ever panic. Just return an UnspecifiedError with some sort of string.
The latter is not a solution to the former. The latter is a solution to libraries having panicking branches. But panics or other logically incorrect behavior can still occur as a result of bugs.
It's pretty difficult to have no panics, because many functions allocate memory and what are they supposed to do when there is no memory left? Also many functions use addition and what is one supposed to do in case of overflow?
Additions are easy. By default they are wrapped, and you can make them explicit with checked_ methods.
Assuming that you are not using much recursion, you can eliminate most of the heap related memory panics by adding limited reservation checks for dynamic data, which is allocated based on user input/external data. You should also use statically sized types whennever possible. They are also faster.
Wrapping on overflow is wrong because this is not the math we expect. As a result, errors and vulnerabilities occur (look at Linux kernel for example).
It depends on the context. Of course the result may cause vulnerabilities if the program logic in bad context depends on it. But yeah, generally I would agree.
Don't know about your parent poster but I didn't take it 100% literally. Obviously if there's no memory left then you crash; the kernel would likely murder your program half a second later anyway.
But for arithmetics Rust has non-aborting bound checking API, if my memory serves.
And that's what I'm trying hard to do in my Rust code f.ex. don't frivolously use `unwrap` or `expect`, ever. And just generally try hard to never use an API that can crash. You can write a few error branches that might never get triggered. It's not the end of the world.
Dealing with integer overflow is much more burdensome than dealing with allocation failure, IME. Relatively speaking, allocation failure is closer to file descriptor limits in terms of how it effects code structure. But then I mostly use C when I'm not using a scripting language. In languages like Rust and C++ there's alot of hidden allocation in the high-level libraries that seem to be popular, perhaps because the notion that "there's nothing you can do" has infected too many minds.
Of course, just like with opening files or integer arithmetic, if you don't pay any attention to handling the errors up front when writing your code, it can be an onerous if not impossible to task to refactor things after the fact.
Oh I agree, don't get me wrong. Both are pretty gnarly.
I was approaching these problems strictly from the point of view of what can Rust do today really, nothing else. To me having checked and non-panicking API for integer overflows / underflows at least gives you some agency.
If you don't have memory, well, usually you are cooked. Though one area where Rust can become even better there is to give us some API to reserve more memory upfront, maybe? Or I don't know, maybe adopt some of the memory-arena crates in stdlib.
But yeah, agreed. Not the types of problems I want to have anymore (because I did have them in the past).
In C I simply use -fsanitize=signed-integer-overflow if I expect no overflow and checked arithmetic when I need to handle overflow. I do not think this is worse than in any other languages and seems less annoying than Rust. If I am lazy, I let allocation failure trap on null pointer dereference which is also safe, out-of-bounds accesses are avoided by -fsanitize=bounds (I avoid pointer arithmetic and unsafe casts where I can and essentially treat it like Rust's "unsafe").
Rust provides a default integer of each common size and signedness, for which overflow is prohibited [but this prohibition may not be enforced in release compiled binaries depending on your chosen settings for the compiler, in this case what happens is not promised but today it will wrap - it's wrong to write code which does this on purpose - see the wrapping types below if you want that - but it won't cause UB if you do it anyway]
Rust also provides Wrapping and Saturating wrapper types for these integers, which wrap (255 + 1 == 0) or saturate (255 + 1 == 255). Depending on your CPU either or both of these might just be "how the computer works anyway" and will accordingly be very fast. Neither of them is how humans normally think about arithmetic.
Furthermore, Rust also provides operations which do all of the above, as well as the more fundamental "with carry" type operations where you get two results from the operation and must write your algorithms accordingly, and explicitly fallible operations where if you would overflow your operation reports that it did not succeed.
>many functions allocate memory and what are they supposed to do when there is no memory left?
Return an AllocationError. Rust unfortunately picked the wrong default here for the sake of convenience, along with the default of assuming a global allocator. It's now trying to add in explicit allocators and allocation failure handling (A:Allocator type param) at the cost of splitting the ecosystem (all third-party code, including parts of libstd itself like std::io::Read::read_to_end, only work with A=GlobalAlloc).
Zig for example does it right by having explicit allocators from the start, plus good support for having the allocator outside the type (ArrayList vs ArrayListUnmanaged) so that multiple values within a composite type can all use the same allocator.
>Also many functions use addition and what is one supposed to do in case of overflow?
Note that for the checked case, it is possible to have a newtype wrapper that impls std::ops::Add etc, so that you can continue using the compact `+` etc instead of the cumbersome `.checked_add(...)` etc. For the wrapping case libstd already has such a newtype: std::num::Wrapping.
Also, there is a clippy lint for disallowing `+` etc ( https://rust-lang.github.io/rust-clippy/master/index.html#ar... ), though I assume only the most masochistic people enable it. I actually tried to enable it once for some parsing code where I wanted to enforce checked arithmetic, but it pointlessly triggered on my Checked wrapper (as described in the previous paragraph) so I ended up disabling it.
> Rust unfortunately picked the wrong default here
I partially disagree with this. Using Zig style allocators doesn't really fit with Rust ergonomics, as it would require pretty extensive lifetime annotations. With no_std, you absolutely can roll your own allocation styles, at the price of more manual lifetime annotations.
I do hope though that some library comes along that allows for Zig style collections, with the associated lifetimes... (It's been a bit painful rolling my own local allocator for audio processing).
Explicit allocators do work with Rust, as evidenced by them already working for libstd's types, as I said. The mistake was to not have them from day one which has caused most code to assume GlobalAlloc.
As long as the type is generic on the allocator, the lifetimes of the allocator don't appear in the type. So eg if your allocator is using a stack array in main then your allocator happens to be backed by `&'a [MaybeUninit<u8>]`, but things like Vec<T, A> instantiated with A = YourAllocator<'a> don't need to be concerned with 'a themselves.
If by Zig-style allocators you specifically mean type-erased allocators, as a way to not have to parameterize everything on A:Allocator, then yes the equivalent in Rust would be a &'a dyn Allocator that has an infectious 'a lifetime parameter instead. Given the choice between an infectious type parameter and infectious lifetime parameter I'd take the former.
Ah, my bad, I guess I've been misunderstanding how the Allocator proposal works all along (I thought it was only for 'static allocators, this actually makes a lot more sense!).
I guess all that to say, I agree then that this should've been in std from day one.
The problem is, everything should have been there since day 1. It’s still unclear which API Rust should end up with, even today, which is why it isn’t stable yet.
Looking forward to the API when it's stabilised.
Have there been any updates on the progress of allocators of this general area of Rust over the past year?
> Rust unfortunately picked the wrong default here for the sake of convenience, along with the default of assuming a global allocator. [...] Zig for example does it right by having explicit allocators from the start
Rust picked the right default for applications that run in an OS whereas Zig picked the right default for embedded. Both are good for their respective domains, neither is good at both domains. Zig's choice is verbose and useless on a typical desktop OS, especially with overcommit, whereas Rust's choice is problematic for embedded where things just work differently.
Various kind of "desktop" applications like databases and video games use custom non-global allocators - per-thread, per arena, etc - because they have specific memory allocation and usage patterns that a generic allocator does not handle as well as targeted ones can.
My current $dayjob involves a "server" application that needs to run in a strict memory limit. We had to write our own allocator and collections because the default ones' insistence on using GlobalAlloc infallibly doesn't work for us.
Thinking that only "embedded" cares about custom allocators is just naive.
I don't see why you would have to write your own - there are plenty of options in the crate ecosystem, but perhaps you found them insufficient?
As a video game developer, I've found the case for custom general-purpose allocators pretty weak in practice. It's exceedingly rare that you really want complicated nonlinear data structures, such as hash maps, to use a bump-allocator. One rehash and your fixed size arena blows up completely.
95% of use cases are covered by reusing flat data structures (`Vec`, `BinaryHeap`, etc.) between frames.
The allocator we wrote for $dayjob is essentially a buffer pool with a configurable number of "tiers" of buffers. "Static tiers" have N pre-allocated buffers of S bytes each, where N and S are provided by configuration for each tier. The "dynamic" tier malloc's on demand and can provide up to S bytes; it tracks how many bytes it has currently allocated.
Requests are matched against the smallest tier that can satisfy them (static tiers before dynamic). If no tier can satisfy it (static tiers are too small or empty, dynamic tier's "remaining" count is too low), then that's an allocation failure and handled by the caller accordingly. Eg if the request was for the initial buffer for accepting a client connection, the client is disconnected.
When a buffer is returned to the allocator it's matched up to the tier it came from - if it came from a static tier it's placed back in that tier's list, if it came from the dynamic tier it's free()d and the tier's used counter is decremented.
Buffers have a simple API similar to the bytes crate - "owned buffers" allow &mut access, "shared buffers" provide only & access and cloning them just increments a refcount, owned buffers can be split into smaller owned buffers or frozen into shared buffers, etc.
The allocator also has an API to query its usage as an aggregate percentage, which can be used to do things like proactively perform backpressure on new connections (reject them and let them retry later or connect to a different server) when the pool is above a threshold while continuing to service existing connections without a threshold.
The allocator can also be configured to allocate using `mmap(tempfile)` instead of malloc, because some parts of the server store small, infrequently-used data, so they can take the hit of storing their data "on disk", ie paged out of RAM, to leave RAM available for everything else. (We can't rely on the presence of a swapfile so there's no guarantee that regular memory will be able to be paged out.)
As for crates.io, there is no option. We need local allocators because different parts of the server use different instances of the above allocator with different tier configs. Stable Rust only supports replacing GlobalAlloc; everything to do with local allocators is unstable, and we don't intend to switch to nightly just for this. Also FWIW our allocator has both a sync and async API for allocation (some of the allocator instances are expected to run at capacity most of the time, so async allocation with a timeout provides some slack and backpressure as opposed to rejecting requests synchronously and causing churn), so it won't completely line up with std::alloc::Allocator even if/when that does get stabilized. (But the async allocation is used in a localized part of the server so we might consider having both an Allocator impl and the async direct API.)
And so because we need local allocators, we had to write our own replacements of Vec, Queue, Box, Arc, etc because the API for using custom A with them is also unstable.
That's public information. It's up to you to make the choice whether to trust someone, but the least you can do is look at the code and see if it matches what you would have done.
> Thinking that only "embedded" cares about custom allocators is just naive.
I said absolutely no such thing? In my $dayjob working on graphics I, too, have used custom allocators for various things, primarily in C++ though, not Rust. But that in no way makes the default of a global allocator wrong, and often those custom allocators have specialized constraints that you can exploit with custom containers, too, so it's not like you'd be reaching for the stdlib versions probably anyway.
>Return an AllocationError. Rust unfortunately picked the wrong default here for the sake of convenience, along with the default of assuming a global allocator. It's now trying to add in explicit allocators and allocation failure handling
Going from panic to panic free in Rust is as simple as choosing 'function' vs 'try_function'. The actual mistakes in Rust were the ones where the non-try version should have produced a panic by default. Adding Box::try_new next to Box::new is easy.
There are only two major applications of panic free code in Rust: critical sections inside mutexes and unsafe code (because panic safety is harder to write than panic free code). In almost every other case it is far more fruitful to use fuzzing and model checking to explicitly look for panics.
In order to have true ergonomic no_panic code in Rust you'd need to be able to have parametricity on the panic behavior: have a single Box::new that can be context determined to be panicky or Result based. It has to be context determined and not explicitly code determined so that the top most request for the no_panic version to be propagated all the way down to stdlib through the entire stack. If you squint just a bit, you can see this is the same as maybe async, and maybe const, and maybe allocate, and maybe wrapping/overflowing math, etc. So there's an option to just add try_ methods on the entire stdlib, which all the code between your API and the underlying API need to use/expose, or push for a generic language level mechanism for this. Which then complicates the language, compiler and library code further. Or do both.
>what are they supposed to do when there is no memory left
Well on Linux they are apparently supposed to return memory anyway and at some point in the future possibly SEGV your process when you happen to dereference some unrelated pointer.
You can tell Linux that you don't want overcommit. You will probably discover that you're now even more miserable and change it back, but it's an option.
Whenever I switch off overcommitting, every program on my system (that I'm using) dies, one by one, over the course of 2–5 seconds, followed by Xorg. It's quite pretty.
> what are they supposed to do when there is no memory left?
You abandon the current activity and bubble up the error to a stage where that effort can be tossed out or retried sometime later. i.e. Use the same error handling approach you would have to use for any other unreliable operation like networking.
Let's say the library panics because there was an out-of-bounds array access on some internal (to that library) array due to a bug in their code. How will you handle this error yourself, and how is the library supposed to propagate it to you in the first place without unwinding?
Ensure all bounds and invariants are checked, and return Result<T, E> or a custom error or something. As I said, I do not want a library to panic. It should be up to the user of the library. When I write libraries, I make sure that the users of the library are able to handle the errors themselves. Imagine using some library, but they use assert() or panic() instead of returning an error for you to handle, that would frustrate me.
This is already a thing, I do this right now. You configure the linter to forbid panics, unwraps, and even arithmetic side effects at compile time.
You can configure your lints in your workspace-level Cargo.toml (the folder of crates)
“””
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -1 }
# arithmetic_side_effects = "deny"
unwrap_used = "deny"
expect_used = "deny"
panic = "deny"
“””
then in your crate Cargo.toml
“””
[lints]
workspace = true
“””
Then you can’t even compile the code without proper error handling. Combine that with thiserror or anyhow with the backtrace feature and you can yeet errors with “?” operators or match on em, map_err, map_or_else, ignore them, etc
There's a lint for indexing an array, but not for all maybe-panicking operations. For example, the `copy_from_slice` method on slices (https://doc.rust-lang.org/std/primitive.slice.html#method.co...) doesn't have a clippy lint for it, even though it will panic if given the wrong length.
The issue with this in practice is that there are always cases where panics are absolutely the correct course of action. When program state is bad enough that you can't safely continue, you need to panic (and core dump in dev). Otherwise you are likely just creating an integrity minefield for you to debug later.
Not saying there aren't applications where using these lints couldn't be alright (web servers maybe), but at least in my experiences (mostly doing CLI, graphics, and embedded stuff) trying to keep the program alive leads to more problems than less.
I wish Option and Result weren’t exclusive. Sometimes a method can return an error, no result or a valid result. Some crates return an error for “no result”, which feels wrong to me. My solution is to wrap Result<Option>, but it still feels clunky.
I could of course create my own type for this, but then it won’t work with the ? operator.
I think Result<Option> is the way to go. It describes precisely that: was it Ok? if yes, was there a value?
I could imagine situations where an empty return value would constitute an Error, but in 99% of cases returning None would be better.
Result<Option> may feel clunky, but if I can give one recommendation when it comes to Rust, is that you should not value your own code-aesthetical feelings too much as it will lead to a lot of pain in many cases — work with the grain of the language not against it even if the result does not satisfy you. In this case I'd highly recommend just using Result<Option> and stop worrying about it.
You being able to compose/nest those base types and unwraping or matching them in different sections of your code is a strength not a weakness.
Well, I think returning "not found" when action performed was an "update X" and X doesn't exist. Result<Option> is totally normal where it makes sense, tho.
People who say "Rust compiling is so slow" have never experienced what building large projects was like in the mid-1990s or so. It's totally fine. Besides, there's also https://xkcd.com/303/
Not really relevant. The benchmark is how other language toolchains perform today, not what they failed to do 30 years ago. I don't think we'd find it acceptable to go back to mid-'90s build times in other languages, so why should we be ok with it with something like Rust?
It depends on where you're coming from. For me, Rust has replaced a lot of Python code and a lot of C# code, so yes, the Rust compilation is slow by comparison. However, it really hasn't adversely affected (AFAICT) my/our iteration speed on projects, and there are aspects of Rust that have significantly sped things up (eg, compilation failures help detect bugs before they make it into code that we're testing/running).
Absolutely, the compile times are the biggest drawback IMO. Everywhere I've been that built large systems in Rust eventually ends up spending a good amount of dev time trying to get CI/CD pipeline times to something sane.
Besides developer productivity it can be an issue when you need a critical fix to go out quickly and your pipelines take 60+ minutes.
Don't use a single monolithic crate. Break your project up into multiple crates. Not only does this help with compile time (the individual crate compiles can be parallelized), it also tends to help with API design as well.
With some exceptions for core data structures, it seems that if you only modified a few files in a large project the total compilation time would be quick no matter how slow the compiler was.
If you have the money to throw at it, you can get a long way optimising CI pipelines just by throwing faster hardware at it. The sort of server you could rent for ~$150/month might easily be ~5x faster than your typical Github Actions hosted runner.
Besides faster hardware, one of the main features (and drawbacks) you get with self-hosted runners is the option to break through build isolation, and have performant caches between builds.
With many other build systems I'd be hesitant to do that, but since Cargo is very good about what to rebuild for incremental builds, keeping the cache around is a huge speed boost.
We have 60 minutes deploy pipelines and are in python. Just mentioning that since, in theory, we are not penalized for long compile times.
Fast ability to quickly test and get feedback is mana from the gods in software development. Organizations should keep it right below customer satisfaction and growth as a driving metric.
I worked in the chromium C++ source tree for years and compiling there was orders of magnitude slower than any Rust source tree I've worked in so far.
Granted, there aren't any Rust projects that large yet, but I feel like compilation speeds are something that can be worked around with tooling (distributed build farms, etc.). C++'s lack of safety and a proclivity for "use after free" errors is harder to fix.
It is slow, and yes it is a problem, but given that typical Rust code generally needs fewer full compiles to get working tests (with more time spent active in the editor, with an incremental compiler like Rust Analyzer) it usually balances out.
Cargo also has good caching out of the box. While cargo is not the best build system, it's an easy to use good system, so you generally get good compile times for development when you edit just one file. This is along made heavy use of with docker workflows like cargo-chef.
Compile times are the reason why I'm sticking with C++, especially with the recent progress on modules. I want people with weaker computers to be able to build and contribute to the software I write, and Rust is not the language for that.
Yes, Rust compiling is slow. Then again, I wouldn't say that C++ is exactly speedy in that area either. Nor Java. None of those are even in the same zip code to Go's compile speed.
So if you're cool with C++ or Java compile times, Rust will generally be fine. If you're coming from Go, Rust compiles will fell positively glacial.
Result type still requires quite a few lines of boilerplate if one needs to add custom data to it. And as a replacement of exceptions with automatic stack trace attachment it is relatively poor.
In any case I will take Rust Result over C++ mess at any time especially given that we have two C++, one with exception support and one without making code incompatible between two.
FWIW, stack traces are part of C++ now and you can construct custom error types that automagically attach them if desired. Result types largely already exist in recent C++ editions if you want them.
I use completely custom error handling stacks in C++ and they are quite slick these days, thanks to improvements in the language.
What I really like to see is stack traces annotated with values of selected local values. A few years ago I tried that in a C++ code base where exceptions were disabled using macros and something like error context passed by references. But the result was ugly and I realized that I had zero chances to adopt it.
With Rust Result and powerful macros it easier to implement.
The Result type isn't really enough for fun and easy error handling. I usually also need to reach for libraries like anyhow https://docs.rs/anyhow/latest/anyhow/. Otherwise, you still need to think about the different error types returned by different libraries.
Back at Google, it was truly an error handling nirvana because they had StatusOr which makes sure that the error type is just Status, a standardized company-wide type that stills allows significant custom errors that map to standardized error categories.
Proper error handling is the biggest problem in a vast majority of programs and rust makes that straightforward by providing a framework that works really well. I hate the `?` shortcut though. It's used horribly in many rust programs that I've seen because the programmers just use it as a half assed replacement for exceptions. Another gripe I have is that most library authors don't document what errors are returned in what situations and you're left making guesses or navigating through the library code to figure this out.
You can write a Rust function that returns `i32` where a negative value indicates an error case. Nothing in Rust prevents you from doing that. But Rust does have facilities that may offer a nicer way of solving your underlying problem.
For instance, a common example of the "integer on error" pattern in other languages is `array.index_of(element)`, returning a non-negative index if found or a negative value if not found. In Rust, the return type of `Iterator::position` is instead `Option<usize>`. You can't accidentally forget to check whether it's present. You could still write your own `index_of(&self, element: &T) -> isize /* negative if not found */` if that's your preference.
Error handling and propagation is one of those things I found the most irritating and struggled[1] with the most as I learned Rust, and to be honest, I'm still not sure I understand or like Rust's way. Decades of C++ and Python has strongly biased me towards the try/except pattern.
Counterpoint: Decades of C++/Python/Java/... has strongly biased me against the try/except pattern.
It's obviously subjective in many ways. However, what I dislike the most is that try/except hides the error path from me when I'm reading code. Decades of trying to figure out why that stacktrace is happening in production suddenly has given me a strong dislike for that path being hidden from me when I'm writing my code.
There should be a way to have the function/method document what sort of stuff can go wrong, and what kinds of exceptions you can get out of it.
It could be some kind of an exception check thing, where you would either have to make sure that you handle the error locally somehow, or propagate it upwards. Sadly programming is not ready for such ideas yet.
---
I jest, but this is exactly what checked exceptions are for. And the irony of stuff like Rust's use of `Result<T, E>` and similarly ML-ey stuff is that in practice they end up with what are essentially just checked exceptions, except with the error type information being somewhere else.
Of course, people might argue that checked exceptions suck because they've seen the way Java has handled them, but like... that's Java. And I'm sorry, but Java isn't the definition of how checked exceptions can work. But because of Java having "tainted" the idea, it's not explored any further, because we instead just assume that it's bad by construction and then end up doing the same thing anyway, only slightly different.
> There should be a way to have the function/method document what sort of stuff can go wrong, and what kinds of exceptions you can get out of it.
The key phrase you're looking for is "algebraic effect systems". Right now they're a pretty esoteric thing only really seen in PL research, but at one point so was most of the stuff we now take for granted in Rust. Maybe someday they'll make their way to mainstream languages in an ergonomic way.
Honestly I'm not even sure that Java checked exceptions are so bad in general compared to Result<T, E>. The amount of verbiage is roughly the same.
Where Java failed is the inability to write generic code that uses checked exceptions - e.g. a higher-order function should be able to say, "I take argument f, and I might throw anything that f() throws, plus E1". But that, as you rightly point out, is a Java problem, not a checked exception problem. In fact, one of the more advanced proposals for lambda functions in Java tackled this exact issue (but unfortunately they went with a simpler proposal that didn't).
I liked checked exceptions. I just think they were overused. Had a CS prof that summed up the optimal case like this:
Programmer's fault: runtime exception
Not programmer's fault: checked exception
Reading from a file but the disk fails? Not programmer's fault. IOException (checked). Missed a null somewhere? Programmer's fault. NullPointerException (unchecked).
Lack of parametrization meant that any interface that could be implemented in a way that could e.g. throw IOException had to declare it on its methods, even if only a single implementation out of several actually used it. And API clients then had to handle those exceptions even if they knew that they never use the interface implementation that could throw.
Or, alternatively, the interface wouldn't declare it as thrown, and then you couldn't implement it in terms of disk I/O without rewrapping everything into unchecked exceptions. A good example of that is java.util.Map, if you try to implement it on top of a file-based value store.
I work in a new-ish C++ codebase (mid-2021 origin) that uses a Result-like type everywhere (folly::Expected, but you get std::expected in C++23). We have a C pre-processor macro instead of `?` (yes, it's a little less ergonomic, but it's usable). It makes it relatively nice to work in.
That said, I'd prefer to be working in Rust. The C++ code we call into can just raise exceptions anywhere implicitly; there are a hell of a lot of things you can accidentally do wrong without warning; class/method syntax is excessively verbose, etc.
IMHO the ugly thing about Result and Option (and a couple of other Rust features) is that they are stdlib types, basic functionality like this should be language syntax (this is also my main critique of 'modern C++').
And those 'special' stdlib types wouldn't be half as useful without supporting language syntax, so why not go the full way and just implement everything in the language?
Rust also started with it as a library, as try!, before ?. There were reasons why it was worth making syntax, after years of experience with it as a macro.
In my experience, a lot of the code is dedicated to "correctly transforming between different Result / Error types".
Much more verbose than exceptions, despite most of the time pretending they're just exceptions (i.e. the `?` operator).
Why not just implement exceptions instead?
(TBH I fully expect this comment to be downvoted, then Rust to implement exceptions in 10 years... Something similar happened when I suggested generics in Go.)
I've only worked in exceptions, so I can't really comprehend the book-keeping required without them. To me it's a separation of concerns: the happy path only involves happy code. The "side channel" for the unhappy path is an exception, with an exception handler at a layer of the abstraction where it's meaningful, yet happy, code. By "happy" I mean code that's simply the direct practical work that's trying to accomplished something, so doesn't need to worry about when things go terribly wrong.
Being blind to the alternative, and mostly authoring lower level libraries, what's the benefit of not having exceptions? I understand how they're completely inappropriate for an OS, a realtime system, etc, but what about the rest? Or is that the problem: once you have the concept, you've polluted everything?
In places where you only want to pass an error to the caller, Rust lets you just add "?" to the call that returns a Result/Option. And the Rust compiler will check that every potential error is handled.
I wouldn't say that it's the tedious part of the language.
There's mostly drawback of having exceptions, I'm not really aware of any benefits of not having them.
People often complain about:
- performance impact, either speed (most languages) or binary size (C++); this, however, is mostly an implementation concern, and doesn't impact Rust at all, as exceptions can simply be syntax-level compiler sugar, and can be compiled exactly the same as Result type is currently (having said that, the optional stack trace is another potential issue, which would have a performance impact even in Rust)
- checked exceptions - this is particularly a concern in Java, which has a fairly poor type system (closed subtyping only) and no type inference, so declaring all unhandled exceptions is tedious
- non-checked exceptions - in this case, "every exception can happen anywhere" so it's ostensibly unsafe (well it's just how life is, and even Java has special unchecked exceptions such as OutOfMemory that can happen anywhere) - some people claim that "exceptions can't just randomly jump out of code" is a benefit of not having exceptions but usually those people sweep OutOfMemory and DivisionByZero under the rug (e.g. Rust, where they just "crash" the program)
Rust would obviously fit the checked exceptions path, as the Result implementatoin basically is "poor-man's checked exceptions". It only needs to flip the syntax sugar - propagate by default (and implicitly), "catch" and materialize using `?` - as well as making Error an open enum (such that you can add cases implicitly - see e.g. OCaml's exception type `exn` [1]) - and that's basically it!
The result type is obviously insufficient for writing nontrivial programs, because nontrivial programs fail in nontrivial ways that need exceptional control flow. The result type does not work because you have to choose between immediate callers handling failures (they don't always have the context to do so because they're not aware of the context of callers higher up on the call stack) or between propagating all of your error values all the way up the stack to the error handling point and making your program fantastically brittle and insanely hard to refactor.
The Result type works for an awful lot of people. Be careful with absolute statements like "does not work." When it works for many others, they might just assume it's a skill issue.
When I say "it doesn't work" I mean that it doesn't allow you to write good code, not "doesn't work" as in the sense that people don't like it. That latter one doesn't make any sense, as languages like PHP "work" for many tens (hundreds?) of thousands of people.
I'm well aware of the tendency of Rust programmers to write bad code, constrained by the language, and then be deluded into thinking that that's good code.
I note that you did nothing to refute my point about why error-handling-via-return-values is insufficient and instead resort to emotional manipulation and logical fallacies.
This seems to happen a lot in the Rust community when people point out flaws in the language.
> When I say "it doesn't work" I mean that it doesn't allow you to write good code
But you skipped over how you are defining, "good code"? Without that part, "doesn't allow" cannot be evaluated in the context of Java or C++ or Python or Go or Rust.
Apparently you have difficulty with reading comprehension, because my original comment already defines "good code":
> The result type does not work because you have to choose between immediate callers handling failures (they don't always have the context to do so because they're not aware of the context of callers higher up on the call stack) or between propagating all of your error values all the way up the stack to the error handling point and making your program fantastically brittle and insanely hard to refactor.
It's interactions like this that are the reason why my organization isn't adopting Rust.
Defined good code TO YOU. You've failed to recognize there is no objective and universally recognized metric for good code within our industry. The closest thing we have to it is number of defects per line of code and how severe those defects are through CVEs. That's it.
You've mistaken your personal preferences and aesthetic sense for absolute truth.
> have to choose between immediate callers handling failures (they don't always have the context to do so because they're not aware of the context of callers higher up on the call stack) or between propagating all of your error values all the way up the stack to the error handling point and making your program fantastically brittle and insanely hard to refactor.
This is a bad choice to have to make. If you believe otherwise, you are an incompetent software engineer. Full stop. These are not personal preferences or aesthetics - these are facts. If you can't parse why these are bad, then you're incapable of basic logic.
> within our industry
Of course, we can already see that because you're resorting to fallacies instead of addressing the point itself.
> You've failed to recognize there is no objective and universally recognized metric for good code within our industry.
Literally none of that is relevant to the point that I'm making. I don't have to define "good code" in order to point out that something is "bad code".
I'm going to print out this thread and show it to anyone who says that they're considering learning Rust as a warning that this is what the community is like - incapable of using logic, unwilling to admit the slightest fault in their religion despite factual evidence to the contrary, and willing to use dishonest rhetoric and fallacies in their defense.
Pointing to programs written with design flaws caused by the flaw in the programming language does invalidate the claim that the flaw exists and negatively affects those programs.
You can write any program in COBOL. Most people would say that it's an insufficient language for doing so.
"Insufficient" here obviously does not mean that it's impossible to write non-trivial programs, just that they'll have bad code.
Convention-wise Go is even better. On the one hand, there is zero magic in error handling ("Errors are values" and interface type 'error' is nothing special), on the other hand it's kind of a convention (slightly enforced by linters) that functions that return errors use this type and it's the last return parameter.
Nothing prevents people from doing their own way (error int codes, bool handling, Result types, etc, panic), but it's just an easiest way that handles well 99% of the error handling cases, so it sticks and gives a nice feeling of predictability of error handling patterns in Go codebases.
It's also highly dependent upon the team's skill and diligence. You can easily ignore errors and skip error handling in Go with predictably hilarious results.
In Rust, you can't just skip error handling. You have to proactively do something generally unwise (and highly visible!) like call .unwrap() or you have to actually handle the error condition.
Go still relies on goodwill and a good night's sleep. The Rust compiler will guard against laziness and sleep deprivation, because ultimately programming languages are about people, not the computers.
Amazing example of how easy it is to get sucked into the rust love. Really sincerely these are really annoying parts of C++.
The conversation function is more language issue. I don’t think there is a simple way of creating a rust equivalent version because C++ has implicit conversions. You could probably create a C++ style turbofish though, parse<uint32_t>([your string]) and have it throw or return std::expected. But you would need to implement that yourself, unless there is some stdlib version I don’t know of.
Don’t conflate language features with library features.
And -Wconversion might be useful for this but I haven’t personally tried it since what Matt is describing with explicit types is the accepted best practice.
Could you elaborate on those points, I'm genuinely curious? So far, I have found the Rust community to be immensely helpful, much more so than I experienced the C++ community. Granted, that's quite some time ago and might be at least partially caused by me asking fewer downright idiotic questions. But still, I'm interested in hearing about your experiences.
The Rust community is helpful... but also quite political and extremely hostile to anyone who doesn't share those politics. Even something as anodyne as saying "let's keep politics out of technical discussion" is frequently met with hostility (because many community members believe that tech is inherently political and that trying to keep politics out is really just a bad faith attempt to frame things in terms of the requester's preferred politics). It's also full of drama in a way that other communities online simply aren't. For example, the drama that happened when the guy behind thephd.dev got invited to give the keynote at rustconf, then his talk was downgraded from the keynote - everyone involved in that mess (including other bloggers who weighed in) came off as immature and not someone you would ever want to work with.
I like the Rust language quite a bit. I find the Rust community to be one of the most toxic places in the entire tech business. Your mileage may vary and that's fine of course - but plenty of people want to stay far away from a community that acts like the Rust community does.
To be clear, everyone in Rust community (on Reddit, Twitter, etc) was shocked by this, and people started asking for explanations. This led to several people stepping down, and seems to have been miscommunication between Rust Foundation and Rust Project.
I'd like to read the other sides of the story; do you have any recommendations?
On the surface it sounds like a community with such deep pathology that it will take at least a generation following a complete change of leadership to have a chance at recovery. But there are three sides to every story.
> On the surface it sounds like a community with such deep pathology
First what sort of pathology? You're confusing community with leadership.
The community didn't want this, and leadership was doing a restructuring due to change from Foundation and Project. Welcome to OSS projects.
Second as opposed to what?
A community at the beck and call of your CEO dictator? I'm a Java dev, so all it takes for Java to die is for One Rich Asshole Called Larry Ellison to decide that they (ORACLE) are inserting two mandatory ads to be watched during each Java compiler run. Or god forbid that they will monetize Java.
Plus if I had 24/7 insight into how Oracle worked, I'd probably also be much less inclined to join Java as a new dev.
To paraphrase Tolstoy: (All perfect languages are dead;) Each imperfect language is imperfect in its own way.
I can try to answer your questions if they are important to you, but it will require some significant effort, so I'd like to be sure they aren't rhetorical first.
Honestly, I'm wondering what kind of pathology do you see that isn't just a community stereotype, and that won't change with the influx of new programmers?
And how does it differ from the average Open-Source Project?
The particular kind of pathology I see here is a strong centralization of the Rust community under the control of an unaccountable leadership caste. The point of free-software licensing is to minimize the control "owners" of software have over their users, protecting those users from abuses by leaders. Somehow Rust users seem to have, to a significant extent, abdicated the rights guaranteed to them by the software license.
For example, in your example of "inserting two mandatory ads to be watched during each Java compiler run", the OpenJDK license guarantees your right to remove those ads from your copy of javac, and to redistribute that copy of javac as the official Debian or Ubuntu version of javac. F-Droid does this routinely to such "antifeatures" in open-source Android applications, and of course Android itself exists because the open-source licenses of projects like Harmony, IDEA, and Eclipse protect Google from such malfeasance.
I'm not sure what the "average open-source project" is. Maybe something like https://github.com/justinasr/FantilatorOS/, the firmware for a redundant array of 64 inexpensive muffin fans that someone set up at CERN? https://web.archive.org/web/20231010215437/https://onlyfans.... Or https://github.com/antimatter15/splat, a Gaussian splat renderer in WebGL? I think those are both "non-average" in that they have a very above-average number of forks and stars. But, like the overwhelming majority of open-source projects, neither of them has a community in any meaningful sense, so I think the question doesn't make sense, given that it's a question about community dynamics.
For where I am concerned, I don't want to have anything to do with the kind of developers that still think that it's acceptable to use Github, VS Code, or Discord in 2025 in a professional setting, much worse teach a new generation of developers to use them : that's like being a doctor and giving out cigarettes to children.
Rust libraries tend to over abstract and then need large refactors when those abstractions fall apart. When I’ve complained about it in the past I’ve been met with “You would need the abstraction eventually”. Maybe, but I’m also capable of building it myself it it gets to that.
Maybe that’s more of a bias with rust media stuff, seems to be going deeper into that rabbit hole though.
The community was at least, may still be, very sensitive to rust being criticised. I genuinely brought an example of a provably correct piece of code that the borrow checker wouldn’t accept, interior mutability problem. I was I should build a massive abstraction to avoid the problem and that I’m holding it wrong… Put me off the language for a few years, it shouldn’t have, I should have just ignored the people and continued on but we all get older and learn things.
I think there's a bit of mismanaged expectations, combined with a community that, while generally helpful, suffers from a bit of fatigue from constantly dispelling myths and falsehoods about the language, often presented in bad faith.
My favorite is when Rust gets dragged into weird American "culture wars" - somehow, it's a "woke" language? (And somehow, that's a problem?)
But yeah, the language docs are pretty up front about the fact that the borrow checker sometimes rejects code that is provably fine, so it's a weird criticism. The nontrivial breakthrough was that Rust proved that a huge amount of nontrivial code can be written within the restrictions of the borrow checker, eliminating swaths of risk factors without a resource penalty.
It's a shame Rust doesn't have keyword arguments or named tuples to make handling some of these things easier without Args/Options structs boilerplate.
Yeah, this is one of the few things that I love about Swift. I think it gets it exactly right that keyword arguments should be the default and you can opt out in cases where the keyword is really unnecessary.
This only really applies to languages that don't check this at compile-time. I don't consider compile-time errors a foot gun. I mean, it should be impossible for that kind of bad code to ever get merged in most reasonable CI/CD processes.
No? This happens in any language that has keyword args.
If I delete/rename a field of a class in any statically checked language, it's going to report a compile error, and it's still a breaking change. Same thing with named arguments.
Sure, but it doesn't actually matter because this can't ever manifest as a bug. Your PR will just be rejected, which to me, is correct behavior.
Typically when you're changing a name you're changing behavior, too. This isn't just something we should let slip under the radar, otherwise bugs can actually manifest.
I guess but you're changing your user-visible API so it should be a breaking change. In languages that don't have this type/arity is all that matters and the name is just nice sugar for the implementor who doesn't have to bind them to useful names.
Even if you don't use keyword args your parameter names are still part of your API surface in Python because callers can directly name positional args. Only recently have you been able enforce unnamed positional only args as well as the opposite.
E.g. OCaml has the ability to separate argument name and variable name for named arguments: let foo ~argument:variable = variable + variable
In fact, I think OCaml has one of the best labeled argument system around there. The only downside is that it doesn't always interact well with currying, but perhaps languages without currying could just copy all the rest.
Just to elaborate a bit, in OCaml you can have functions like:
let foo ~a ?b c =
let b = match b with
| None -> 42
| Some x -> x
in a + b + c
And you can then call this like foo ~a:1 ~b:42 55 or foo ~a:2 ?b:None 55. But then forwarding those optional parameters works like:
let bar ~a ?b c =
foo ~a ?b c
and the optional parameter b will be forwarded as an optional parameter.
Given Rust's historical relations with OCaml I'm slightly disappointed that it doesn't have the same labeled and optional argument system.
Rust Tuples can be destructured into named variables, or Enums can be used as Monads which give a label to a tuple of variables. Rust Enums are real fun to use so I encourage you to dive in. https://doc.rust-lang.org/rust-by-example/custom_types/enum....
Agreed, coming from Python it's one of the main things I miss in Rust. You can achieve something similar with the builder pattern or with structs + the Default trait, but it takes much more effort.
There is '-Wconversion' to catch things like this. It will however not trigger in this specific case since g++ assumes converting 1000.0 to 1000 is ok due to no loss in precision.
Quantity(100) is counterproductive here, as that doesn't narrow the type, it does the opposite, it casts whatever value is given to the type, so even Quantity(100.5) will still work, while just plain 100.5 would have given an error with '-Wconversion'.
My opinion is that if you need to run extra tools/linters in order to catch basic errors, the language & its compiler are not doing enough to protect me from correctness bugs.
I do run clippy on my Rust projects, but that's a matter of style and readability, not correctness (for the most part!).
How much of what Rust the language checks is actually linter checks implemented in the compiler?
Conversions may be fine and even useful in many cases, in this case it isn’t. Converting to std::variant or std::optional are some of those cases that are really nice.
To be clear, the only difference between Rust and C here is whether the conversion happens by default or not. Rust doesn't do the conversion by default but will let you do it if you want to, with `as`.
There are also more type-safe conversion methods that perform a more focused conversion. Eg a widening conversion from i8 -> i16 can be done with .into(), a narrowing conversion from i16 -> i8 can be done with .try_into() (which returns a Result and forces you to handle the overflow case), a signed to unsigned reinterpretation like i64 -> u64 can be done with .cast_unsigned(), and so on. Unlike `as` these have the advantage that they stop compiling if the original value changes type; eg if you refactor something and the i8 in the first example becomes an i32, the i32 -> i16 conversion is no longer a widening conversion so the `.into()` will fail to compile.
Yes that's correct, for exactly the reason that it is more likely to keep compiling and possibly not do what you intended if the original value's type changes due to refactoring. However there are still a few conversions that don't have alternatives to `as` - truncating conversions (eg i64 -> i32 that intentionally discards the upper half), int <-> float conversions (eg i64 -> f64, both truncating and checked conversions), unsized pointer casts (eg *const [MaybeUninit<u8>] -> *const [u8], `.cast()` only works for Sized target), and probably a few more.
Setting up clang-tidy for your IDE isn't really any more trouble than setting up a LSP. If you want the compiler/linter/whatever to reject valid code to protect you from yourself, there are tools you can use for that. Dismissing them just because they aren't part of the language (what, do you expect ISO C++ to enforce clang-tidy usage?) is silly.
The reason certain warnings are on or off by default in compilers in certain warnings modes depends on whether enough people find them useful enough or not. Rust caters to people who want strictness which makes it annoying to use for others, but if you want this you can also - to a large degree - have this in C and C++.
Right. I attempted using Rust for trading-related code as well. However, I failed to write a dynamically linked always sorted order book where you can splice orders in the middle. It is just too dynamic for Rust. Borrow checker killed me.
And don't get me started on dynamic graphs.
I would happily use Rust over C++ if it had all other improvements but similar memory management. I am completely unproductive with Rust model.
In my experience you need to approach this with vec or arrays of some sort and pass indices around… “We have pointers at home” behaviour. This is fine but coming from C++ it definitely feels weird…
Why not just use pointers? Rust has them, they aren't evil or anything. If you need to make a data structure that isn't feasible with references due to the borrow checker (such as a linked list), there's absolutely nothing wrong with using pointers.
And? I don't really see the issue. It works, it is sound, and it has a nice clean interface for safe code to use. That's all I really ask for. Lots of useful things in programming are quite gnarly under the hood, but that doesn't mean those things aren't worth using.
It is fine, there is just not much Rust safety advantage left then. Also in C/C++ the errors do not usually occur when using a nicely defined API, but when doing the low-level gnarly stuff and getting it wrong. As said before, I think there is some advantage of Rust having a safe and unsafe subset, but is is nowhere as big as people claim it is.
> It is fine, there is just not much Rust safety advantage left then.
There's exactly as much as there was before though. The entire point of the Rust safety paradigm is that you can guarantee that unsafe code is confined to only where it is needed. Nobody ever promised "you will never have to write unsafe code", because that would be clearly unfeasible for the systems programming domain Rust is trying to work in.
I frankly cannot understand why people are so willing to throw the baby out with the bathwater when it comes to Rust safety. It makes no sense to me to say "my code needs to have some % unsafe, so I'll just make it 100% unsafe then" (which is effectively what one does when they use C or C++ instead). Why insist on not taking any safety gains at all when one can't have 100% gain?
The fallacy is believing that C / C++ code is 100% unsafe. Yes, Rust propaganda repeats this over and over but a good abstraction in C / C++ will also give you good safety properties. The safe Rust code over gnarly unsafe Rust code is only a little bit better than a nice C / C++ abstraction over gnarly code.
The last fifty years have shown us that you can't — at least not sustainably and at scale. You can do so at one moment in time, or for smaller projects, but not in the face of large scale projects with team members changing over time.
We have to do arbitrary insertions/deletions from the middle, many of them. I think it is more like BTreeMap, but we need either sorting direction or rev(), and there were some problems with both approaches I tried to solve, but eventually gave up.
I see! The big issue I've run into with BTreeMap is that you can't provide an external comparator. If comparisons only require data that the keys already have, then the Reverse wrapper [1] has worked well for me.
I think this is true initially and Rust didn't "click" for me for a long time.
But once you are _maintaining_ applications, man it really does feel like absolute magic. It's amazing how worry-free it feels in many respects.
Plus, once you do embrace it, become familiar, and start forward-thinking about these things, especially in areas that aren't every-nanosecond-counts performance-wise and can simply `Arc<>` and `.clone()` where you need to, it is really quite lovely and you do dramatically less fighting.
Rust is still missing a lot of features that other more-modern languages have, no doubt, but it's been a great ride in my experience.
The idea with Rust is that you get safety...not that you get safety at the cost of performance. The language forces you into paying a performance cost for using patterns when it is relatively easy for a human to reason about safety (imo).
You can use `unsafe` but you naturally ask yourself why I am using Rust (not rational, but true). You can use lifetimes but, personally, every time I have tried to use them I haven't been able to indicate to the compiler that my code is actually safe.
In particular, the protections for double-free and free before use are extremely limiting, and it is possible to reason about these particular bugs in other ways (i.e. defer in Go and Zig) in a way that doesn't force you to change the way you code.
Rust is good in many ways but the specific problem mentioned at the top of this chain is a big issue. Just saying: don't use this type of data structure unless you pay performance cost isn't an actual solution to the problem. The problem with Rust is that it tries to force safety but doesn't have good ways for devs to tell the compiler code is safe...that is a fundamental weakness.
I use Rust quite a bit, it isn't a terrible language and is worth learning but these are big issues. I would have reservations using the language in my own company, rather than someone else's, and if I need to manage memory then I would look elsewhere atm. Due to the size of the community, it is very hard not to use Rust too (for example, Zig is great...but no-one uses it).
The idea with rust is that you _can_ have safety with no performance cost if you need it, but depending on what you're building, of course, that may imply extra work.
The pragmatism of Rust means that you can use reference counting if it suits your use case.
Unsafe also doesn't mean throwing out the Rustiness of Rust, but others have written more extensively about that and I have no personal experience with it.
> The problem with Rust is that it tries to force safety but doesn't have good ways for devs to tell the compiler code is safe...that is a fundamental weakness.
My understanding is that this is the purpose of unsafe, but again, I can't argue against these points from a standpoint of experience, having stuck pretty strictly to safe Rust.
Definitely agree that there are issues with the language, no argument there! So do the maintainers!
> if I need to manage memory then I would look elsewhere atm
Haha I have the exact opposite feeling! I wouldn't try to manage memory any other way, and I'm guessing it's because memory management is more intuitive and well understood by you than by me. I'm lazy and very much like having the compiler do the bulk of the thinking for me. I'm also happy that Rust allows for folks like me to pay a little performance cost and do things a little bit easier while maintaining correctness. For the turbo-coders out there that want the speed and the correctness, Rust has the capability, but depending on your use case (like linked lists) it can definitely be more difficult to express correctness to the compiler.
Agree, that is the purpose of unsafe but there is a degree of irrationality there, which I am guilty of, about using unsafe in Rust. I also worry about unsafe leaking if I am using raw pointer on a struct...but stdlib uses a lot of unsafe code, so I should be too.
I think the issue that people have is that they come into Rust with the expectation that these problems are actually solved. As I said, it would be nice if lifetimes weren't so impossible to use.
The compiler isn't doing the thinking if you have to change your code so the compiler is happy. The problem with Rust is too much thinking: you try something, compiler complains, what is the issue here, can i try this, still complain, what about this, etc. There are specific categories of bugs that Rust is trying to fix that don't require the changes that Rust requires in order to ensure correctness...if you use reference counter, you can have more bugs.
The nice thing is that you can always drop down to unsafe and use raw pointers if your data structure is truly not suited to Rust's ownership rules.
And while unsafe Rust does have some gotchas that vanilla modern C++ does not, I would much rather have a 99% memory-safe code base in Rust than a 100% "who knows" code base in C++.
I have read the "too many linked lists" story and I think the other commenters here are right; the less pointers the better. Even with unsafe, there's just too much ceremony.
Coming from python (or Common Lisp, or...), I wasn't too impressed. In Python I normally make args for any function with more than a couple be keyword arguments, which guarantees that you are aware of how the arguments are being mapped to inputs.
Even Rust's types aren't going to help you if two arguments simply have the same types.
Just create dummy wrappers to make a type level distinction.
A Height and a a Width can be two separate types even if they’re only floats basically.
Or another (dummy) example transfer(accountA, accountB). Make two types that wrap the same type but one being a TargetAccount and the other SourceAccount.
Do you really want width and height or do you actually want dimensions or size? Same with transfer, maybe you wanted a transaction that gets executed. Worst case here use a builder with explicit function names.
Sound type systems are equivalent to proof systems.
You can use them to design data structures where their mere eventual existence guarantee the coherence and validity of your program’s state.
The basic example is “Fin n” that carries at compile time the proof that you made the necessary bounds checks at runtime or by construction that you never exceeded some bound.
Some languages allow you to build entire type level state machines! (eg. to represent these transactions and transitions)
My point is a Width type is usually not the sound type you are looking for. What probably wanted was a size type which is width and height. Or a dimensions type which is width and height. The problem was maybe not two arguments being confused but in reality a single thing with two elements…
What sold me on Rust is that I'm a very bad programmer and I make a lot of mistakes. Given C++, I can't help but hold things wrong and shoot myself in the foot. My media C++ coding session is me writing code, getting a segfault immediately, and then spending time chasing down the reason for that happening, rinse and repeat.
My median Rust coding session isn't much different, I also write code that doesn't work, but it's caught by the compiler. Now, most people call this "fighting with the borrow checker" but I call it "avoiding segfaults before they happen" because when I finally get through the compiler my code usually "just works". It's that magical property Haskell has, Rust also has it to a large extent.
So then what's different about Rust vs. C++? Well Rust actually provides me a path to get to a working program whereas C++ just leaves me with an error message and a treasure map.
What this means is that although I'm a bad programmer, Rust gives me the support I need to build quite large programs on my own. And that extends to the crate ecosystem as well, where they make it very easy to build and link third party libraries, whereas with C++ ld just tells you that it had a problem and you're left on your own to figure out exactly what.
Using your media example since I have a decent amount of experience there. Did you just use off the shelf libraries, because effectively all the libraries are written in or expose a C api. So now you not only need to deal with Rust, you need to also deal with rust ffi.
There are some places I won’t be excited to use rust, and media heavy code is one of those places…
> Given C++, I can't help but hold things wrong and shoot myself
Give an example. I have been programming in C/C++ for close to 30 years and the places where I worked had very strict guidelines on C++ usage. We could count the number of times we shot ourselves due to the language.
Isn't that their point though? They don't have 30 years of C/C++ experience and a workplace with very strict guidelines. They are just trying to write some code, and they run into trouble on C++'s sharper edges.
Low level languages always came with a set of usage guidelines. Either you make the language safe for anyone that they can't shoot themselves in the foot and end up sacrificing performance, or provide guidelines on how to use it while retaining the ability to extract maximum performance from the hardware.
C/C++ shouldn't be approached like programming in Javascript/Python/Perl.
I think it is a bit myth that Rust does not sacrifice performance. If you stick to the safe part Rust usually does not seem to achieve the performance of C/C++ according to what I have seen and read. I agree that the cleaner separation of unsafe and safe parts is an advantage of Rust.
The clean separation between Unsafe and Safe rust is precisely, IMO, one of its biggest advantages (and the reason why it'll be superior to almost anything C++ comes up with).
I see an article about how strict typing is better, but what would really be nice here is named parameters. I never want to go back to anonymous parameters.
Yes, this is one of the few things that I think was a big mistake in Rust's language design. I used to do a lot of Scala, and really liked named parameters there.
I suppose it could still be added in the future; there are probably several syntax options that would be fully backward-compatible, without even needing a new Rust edition.
I suppose the sense it is backwards incompatible is that library authors have named their parameters without intended to make them part of the public interface that they commit to maintaining. Perhaps it could be made backwards compatible by being opt in function declarations but that would seem like a bit of a pain.
> When there are 3-4 parameters it is too much trouble to write the names.
Sorry, I don't agree.
First, code is read far more often than written. The few seconds it takes to type out the arguments are paid again and again each time you have to read it.
Second, this is one of the few things that autocomplete is really good at.
Third, almost everybody configures their IDE to display the names anyway. So, you might as well put them into the source code so people reading the code without an IDE gain the benefit, too.
Finally, yes, they are redundant. That's the point. If the upstream changes something and renames the argument without changing the type I probably want to review it anyway.
They aren't even necessarily redundant. If you have argument names as part of the function name, they can be overloaded on - and this is much more readable than type-based overloading because it's all explicit. Swift uses this to great effect, e.g. here are some different ways to construct a string:
Why? In 2025 we have tooling available for most every editor that will annotate that information into the display without needing them present in the file. When I autocomplete a function name, all the parameters are there for me to fill in, and annotated into the display afterwards. It seems like an unnecessary step to reify it and force the bytes to be present in the the saved file.
This seems a big silly. This is not a language issue. You can have a C++ library that does exactly all the things being shown here so that the application developer doesn't worry about. There would no C++ language features missing that would accomplish what you're able to do on the Rust side.
So is this really a language comparison, or what libraries are available for each language platform? If the latter, that's fine. But let's be clear about what the issue is. It's not the language, it's what libraries are included out of the box.
Just like language shapes the way we think and talk about things, programming languages shape both what libraries are written and how. You could write anything in anything so long as it's Turing complete, but in real life we see clearly that certain design decisions at the language level either advantage or disadvantage certain types of solutions. Everyone could in theory write C without any memory issues, but we all know how that turns out in practice. The language matters.
The strong hypothesis has been debunked, yes, but nobody is asserting it.
From your link:
> Nevertheless, research has produced positive empirical evidence supporting a weaker version of linguistic relativity:[5][4] that a language's structures influence a speaker's perceptions, without strictly limiting or obstructing them.
The core of this argument taken to its extreme kind of makes the whole discussion pointless, right? All the languages can do all the things, so why bother differentiating them?
To entertain the argument, though, it may not be a language issue, but it certainly is a selling point for the language (which to me indicates a "language issue") to me if the language takes care of this "library" (or good defaults as I might call them) for you with no additional effort -- including tight compiler and tooling integration. That's not to say Rust always has good defaults, but I think the author's point is that if you compare them apples-to-oranges, it does highlight the different focuses and feature sets.
I'm not a C++ expert by any stretch, so it's certainly a possibility that such a library exists that makes Rust's type system obsolete in this discussion around correctness, but I'm not aware of it. And I would be incredibly surprised if it held its ground in comparison to Rust in every respect!
That's an entirely different line of reasoning from the article though, and "just git gud" isn't really the solution here any more than it is to use Rust. There are facilities for avoiding these problems that you don't have to learn how to construct yourself in either language.
Sure, you can emulate some of the features and hope that everyone using your library is doing it "right". Just like you could just use a dynamic language, tag every variable with a type, and hope everyone using your library does the MANUAL work of always doing it correct. Guess we don't need types either.
And while we're at it, why not use assembly? It's all just "syntactic sugar" over bits, doesn't make any difference, right?
Eh, maybe. There's a performance tradeoff here and maintainers opted for performance. I'm sure many folks would agree with you that it was the wrong choice, and I'm sure many folks would disagree with you that it was the wrong choice.
There are also specific methods for doing *erflow-checked arithmetic if you like.
Actually, the x86 'ADD' instruction automagically sets the 'OF' and 'CF' flags for you for signed and unsigned overflow respectively [0]. So all you need to do to panic would be to follow that with an 'JO' or 'JB' instruction to the panic handling code. This is about as efficient as you could ask for, but a large sequence of arithmetic operations is still going to gum up the branch predictor, resulting in more pipeline stalls.
If you obscure the implementation a bit, you can change GP's example to a runtime overflow [0]. Note that by default the checks will only occur when using the unoptimized development profile. If you want your optimized release build to also have checks, you can put 'overflow-checks = true' in the '[profile.release]' section of your cargo.toml file [1].
This is bad because the program behaves different depending on build flags. What's the point of having "use unsafe addition" flag, and having it enabled by default?
Rust developers made a poor choice. They should have made a special function for unchecked addition and have "+" operator always panic on overflow.
The point I'm sure was to prevent the checks from incurring runtime overhead in production. Even in release mode, the overflow will only wrap rather than trigger undefined behavior, so this won't cause memory corruption unless you are writing unsafe code that ignores the possibility of overflow.
The checks being on in the debug config means your tests and replications of bug reports will catch overflow if they occur. If you are working on some sensitive application where you can't afford logic bugs from overflows but can afford panics/crashes, you can just turn on checks in release mode.
If you are working on a library which is meant to do something sensible on overflow, you can use the wide variety of member functions such as 'wrapping_add' or 'checked_add' to control what happens on overflow regardless of build configuration.
Finally, if your application can't afford to have logic bugs from overflows and also can't panic, you can use kani [0] to prove that overflow never happens.
All in all, it seems to me like Rust supports a wide variety of use cases pretty nicely.
Rust does have checked arithmetic operations (that return Result), but you have to explicitly opt in to them, of course, and they're not as ergonomic to use as regular arithmetic.
That's not a wrapping addition, that addition will never overflow so it has no overflow behaviour.
This line could only overflow after we need to grow the container, so immediately this means the type T isn't a ZST as the Vec for ZSTs doesn't need storage and so it never grows.
Because its not a ZST the maximum capacity in Rust is never bigger than isize::MAX which is an entire binary order of magnitude smaller than usize::MAX, as a result len + 1 can't overflow the unsigned type, so this code is correct as written.
Rust is the only language I can easily control how integer overflow should behave. I can use `var1.wrapping_add(var2)` if I want the result to be wrapped or `var1.checked_add(var2)` if I don't want it to overflow.
The functions are so verbose and inconvenient that even Rust developers themselves do not use them. For example, in this code [1] they used a wrapping addition instead of "checked_add" because it is faster to write.
For comparison, Swift uses "+" for checked addition and as a result, majority of developers use checked addition by default. And in Rust due to its poor design choices most developers use wrapping addition even where a checked addition should be used.
Checked addition by default will have too much overhead and it will hurt performance, which unacceptable in Rust since it was designed as a system language. Swift can use checked add by default since it was designed for application software.
Your example code is not because it is faster to write, it is because it is impossible for its to overflow on that line.
Why should checked addition have any overhead? You should just use checked addition instruction (on architectures that support it) instead of wrapping addition.
Or just because on Intel CPUs it has overhead, we must forget about writing safer code?
The reason '+ 1' is fine in the example you gave is that length is always less than or equal to capacity. If you follow 'grow_one' which was earlier in the function to grow the capacity by one if needed, you will find that it leads to the checked addition in [0], which returns an error that [1] catches and turns into a panic. So using '+1' prevents a redundant check in release mode while still adding the check in debug mode in case future code changes break the 'len <= capacity' invariant.
Of course, if you don't trust the standard library, you can turn on overflow checks in release mode too. However, the standard library is well tested and I think most people would appreciate the speed from eliding redundant checks.
Rust does seem to have a lot of nice features. My biggest blocker for me going to Rust from C++ is that C++ has much better support for generic programming. And now that Concepts have landed, I'm not aware of any language that can compete in this area.
The C++ code I write these days is actually pretty similar to Rust: everything is explicit, lots of strong types, very simple and clear lifetimes (arenas, pools), non-owning handles instead of pointers. The only difference in practice is that the build systems are different and that the Rust compiler is more helpful (both in catching bugs and reporting errors). Neither a huge deal if you have a proper build and testing setup and when everybody on your team is pretty experienced.
By the way, using "atoi" in a code snippet in 2025 and complaining that it is "not ideal" is, well, not ideal.
Meaning you're in a context where you have control on the C++ code you get to write. In my company, lots of people get to update code without strict guidelines. As a result, the code is going to be complex. I'd rather have a simpler and more restrictive language and I'll always favor Rust projects to C++ ones.
That is easy to say today, but I guarantee in 30 year Rust will have rough edges too. People always want some new feature and eventually one comes in that cannot be accommodated nicely.
Of course it will probably not be as bad as C++, but still it will be complex and people will be looking for a simpler language.
Rust has rough edges today. The field of programming is still only a few decades old, and there's no doubt that something even shinier will come along, we just don't know yet what that looks like.
That's not a good reason to stick with inferior tools now, though.
Rust is inferior to C++ for my needs. This is just a reflection on we started a large project in C++ before rust existed, and now have millions of lines. Getting Rust to work with our existing C++ is hard enough as to not be worth it. Rewriting in Rust would cost 1 billion dollars. Thus despite all the problems we have with C++ that Rust would solve, rust is inferior.
(Rust is working on their C++ interoperability story and we are making changes that will allow using Rust in the future so I reserve the right to change this story in a few years, but only time will tell)
I don't think it's a defect of the language that your particular circumstance makes it infeasible to port your project. Having a great C++ interop story would be amazing, but Rust would be decidedly less awesome if it had made concessions in that direction early on.
There's a lot of warts here, particularly around the fact that all Rust types are "trivially relocatable" in C++ parlance. At the same time, figuring out which C++ types are trivially relocatable is pretty difficult. To give you an idea of the current situation, all or most non-POD C++ types must be "pinned" on the Rust side, forcing you to deal with the rather clunky `Pin<&mut T>` API. Either that or heap-allocating all C++ types owned from Rust code. Not great.
I didn't say it was a defect. I said it made rust inferior for me. Those are different things. I don't disagree with rust reasons for making that tradeoff - but the tradeoff as a result makes rust useless for now, for me. If it works for you great.
I think the term "inferior" isn't really appropriate, then. It typically applies to the quality of the thing on its own, not its applicability outside of its stated scope.
Unfortunely thanks to the "code C in C++ crowd", there is this urban myth that goodies like proper encapsulation, stronger types, RAII, were not even available in pre-C++98, aka C++ARM.
Meanwhile it was one of the reasons after Turbo Pascal, my next favourite programming language became C++.
For me mastering C, after 1992, only became mattered because as professional, that is something that occasionally I have to delve into so better know your tools even if the grip itself has sharp corners, otherwise everytime the option was constrained to either C or C++, I always pick C++.
The strong/weak distinction is a bit fuzzy, but reasonable people can have the opinion that C++ is, in fact, loosely/weakly typed. There are countless ways to bypass the type system, and there are implicit conversions everywhere.
It _is_ statically typed, though, so it falls in a weird category of loosely _and_ statically typed languages.
I think that explicit casts really ought to be discounted, since if you're writing one, you are simply getting what you have asked for. This would be like saying that e.g. Modula-2 is weakly typed because it has bitcast.
That aside, the only remaining footgun in C++ is the implicit numeric conversions. What else did you have in mind?
I mean, the default behavior of single-argument constructors in C++ is implicit conversion. You have to opt into explicit conversions using the `explicit` keyword on constructors and assignment operators.
Then you have all the shenanigans around placement-new and vtables.
If it isn't downright weak, it's also not particularly strong.
The issue with constructors is a real one, yeah. Although forcing `explicit` on single-argument constructors is a single linter rule (which is a good idea for this exact reason...).
OTOH placement-new is pretty much impossible to use by accident. If used intentionally, I don't see it as being any different from an explicit cast - again, you get what you signed up for.
Interestingly in some ways C++ is arguably more typesafe than languages like Java or C#, given how it handles dynamic type of object during construction & destruction...
I still find it basically impossible to get started with a C++ project.
I tried again recently for a proxy I was writing thinking surely things have evolved at this point. Every single package manager couldn’t handle my very basic and very popular dependencies. I mean I tried every single one. This is completely insane to me.
Not to mention just figuring out how to build it after that which was a massive headache and an ongoing one.
Compared to Rust it’s just night and day.
Outside of embedded programming or some special use cases I have literally no idea why anyone would ever write C++. I’m convinced it’s a bunch of masochists
> Every single package manager couldn’t handle my very basic and very popular dependencies
Well there's your problem - no serious project uses one.
> I’m convinced it’s a bunch of masochists
People use cpp because it's a mature language with mature tooling and an enormous number of mature libraries. Same exact reason anyone uses any language for serious work.
How can you simultaneously call cpp a mature language with mature tooling and acknowledge that there's no working package manager used by any "serious" project?
Do you people really not realize how completely asinine you sound with these lowbrow comments? I'll give you a hint: did you know that C also has no package manager?
Yeah, and it's also much worse for it. There's a reason everyone in C uses their own linked list implementation and it's not because it's a platonic ideal of perfect software.
Agreed. Getting started with a C or C++ project is such a pain in the ass that I won't even bother. Then there is the fact that unless you have special requirements that necessitate C/C++, those languages have nothing going for them.
"Mature" means that it has mostly developed to its logical conclusion, and its warts are well-documented and understood, as are workarounds for them. It doesn't say anything about "good", although using mature tooling can be good because (for all the warts) you don't have the rug pulled from underneath you every couple of years, as seen in e.g. JS land.
I think C/C++ are mature in the sense that everyone has gotten tired of trying to fix these issues. But I think we're getting further and further from "well understood" as time goes on. Very few practicing software engineers understand all the nuances of header files, C files, object files, static and dynamic linking, and how different compilers & ABI targets interact with all of that stuff. To say nothing of configure scripts, makefiles, automake & autoconf, pkgconfig, cmake and ninja and all the rest.
Just today, I spent 2 hours trying to get an OS kernel (SeL4) to compile for ARM64. This is an officially supported platform. I ran into a series of problems:
- A recent update to sel4 meant I need to set -DCONFIG_ARM_TLS_REG_TPIDRU for thread-local storage. (Leaving it out is a compiler error). This flag isn't in the configuration script - and it took me awhile to figure out how to set it through cmake + ninja.
- The gcc-aarch64-linux-gnu toolchain was recommended somewhere in the docs. On newer versions of gcc, that pulls in libgcc_eh.a, which tries to use _dl_find_object - which is of course linux only. So the build fails.
- So I swapped to gcc14's aarch64-none-elf cross compiler toolchain, which doesn't assume a dynamic linker is available. gcc-aarch64-none-elf isn't available in apt for some reason, even though the 32 bit counterpart is. From there, I ran into a new compiler error. Turns out the arm gcc toolchains are built against an old version of binutils that didn't have support for SHF_GNU_RETAIN. That is needed for the retain macro to work in C. And retain is used by sel4. This is just a compiler warning, but because sel4 promotes all warnings to errors, the build fails. I could have built my own compiler with a newer binutils. But I ended up just disabling the warning, because you only live once. I briefly tried gcc11 because that's recommended elsewhere in the docs, but gcc11 has the same problem.
All of this headache was from trying to build sel4 - which is all straight C, and one of the highest quality C projects I've ever seen. The codebase is mathematically proven to be correct. But oh my goodness, what a horrendous mess it is trying to work with it. And of course, just to try and get the build process going, I needed to install multiple compilers, ninja, cmake, python and a bunch of random python packages, and google's 'repo' tool to manage dependencies. What is this junk.
Personally I'd prefer to work with clang, but I've had enough pain for one day. And I know swapping compilers will for sure break something else.
If sel4 was written in zig or rust, it would be 10x easier to build and work with. Zig handles cross compilation to any supported platform out of the box. Rust can install toolchains trivially with rustup. Rust also has consistent stability guarantees, ensuring code doesn't "rot" with new compiler versions. And of course, both handle dependencies much, much better. And no need for cmake / ninja / random python scripts to compile. Zig and rust also don't behave differently based on someone else's binutils version. Wtf.
If C is too "mature" to fix these problems, I'm going to switch languages. Sel4 is only 20k lines of code. I wonder if it'd be less work to port it to zig than work with it in C. (I'd consider rust, but rust code can't prevent panics.)
Package managers per language are a (relatively) new endeavor. The oldest language I can think of that widely adopted it was Perl. Although, perl was quite ahead of it's time in a lot of ways, and php undid some the work of perl and went back to popularizing include type dependencies instead of formal modules with a package manager.
C++ "gets away" with it because of templates. Many (most?) libraries are mostly templates, or at the very least contain templates. So you're forced into include-style dependencies and it's pretty painless. For a good library, it's often downloading a single file and just #include-ing it.
C++ is getting modules now, and maybe that will spur a new interest in package managers. Or maybe not, it might be too late.
It's a relatively new endeavor, but it's also a requirement in 2025 if you want to be portable. The Linux ecosystem was focusing on installing dependencies system-wide for decades (that's how traditional `./configure.sh` expects things to work), and this approach is just inferior in so many ways.
The shenanigans people get into with CMake, Conan, vcpkg, and so on is a patchwork of nightmares and a huge time sink compared to superior solutions that people have gotten used to in other languages, including Rust.
I don't think it's a requirement and it does actually come with it's own set of foot guns. The problem is that these design decisions impact culture, which is why JS has a culture of importing far too many third-party packages. Which comes with it's own set of risks.
I agree overall that the C or C++ way of doing things is more cumbersome, but I don't think that's enough to write off those languages as a whole.
C++ is _getting_ modules now? You must be kidding. How can this language, that adds more and more and more features every couple of years _still_ not have one of the most fundamental way to modularize code, splitting it into semantic units? Like ... what?! I did not think C++ would be that bad when it comes to foundational aspects of a programming language. This must be the result of silly obsession with OOP. The typical misuse of "we have classes, we don't need modules!". What other explanation could there possibly be for lacking such basic means of code organization.
Modules were added in C++20, but it's taken them this long to be implemented. Right now, only MSVC has full support according to https://en.cppreference.com/w/cpp/20
C++ build systems are notoriously brittle. When porting a project to a new platform, you're never just porting the code, you are also porting your build system. Every single project is bespoke in some way, sometimes because of taste, but most of the time because of necessity.
It works because people spend a huge amount of time to make it work.
This seems hyperbolic. At work we cross compile the same code for a decent number of different platform - six different OS (Linux Mac windows and some embedded ones) over 20odd cpu architectures.
When I've dabbled in C++ recently it has felt like using CMake fetching github repos has been the least painful thing I've tried (dabbled in vcpkg and conan a bit), since most libraries are cmake projects.
I am no expert so take it with a grain of salt, but that was how it felt for me.
I was using fetch content or the like, there is a package that comes after a certain version of cmake where you can tell it this is a git repo and it handles all that for you. It has been a few months since I did this so I don't remember the details fully
You can have two arguments that are semantically as distinct and important as quantity and price and be both integers, and if you swap them is a big issue anyway. And you would be forced, if you like this kind of programming, to create distinct types anyway. But I never trust this kind of "toy" defensive programming. The value comes from testing very well the code, from a rigorous quality focus.
To be fair, this sort of thing doesn't have to be so much worse in C++ (yes, it would have been nice if it had been built into the language itself to begin with). You just need a function to do a back-and-forth conversion which then double-check the results, ie:
Yes, from safety point of view Rust is much better option, however from the ecosystems I care about (language runtimes and GPU coding), both professionally and as hobby, C++ is the systems language to go, using Rust in such contexts would require me to introduce extra layers and do yak shaving instead of the actual problem that I want to code for.
Well, precisely. Such is the price of "general-purpose safety". The biggest irony IMO is that the security features of Rust are so often skirted in the name of speed, efficiency, etc, with the end result being a program which isn't much more secure than its C++ equivalent. ¯\_(ツ)_/¯
> The biggest irony IMO is that the security features of Rust are so often skirted in the name of speed, efficiency, etc, with the end result being a program which isn't much more secure than its C++ equivalent.
Citation needed. The empiric evidence I’ve seen has shown the opposite.
Again, not a criticism of the language itself per se, and certainly not trying to imply that all Rust programmers go around writing unsafe code all day long. It's just kind of funny to me that it is touted as being so much more safe than C++ when the reality is that at least some of those features are in fact disabled for one reason or another in real-world programs.
What are you trying to say? C++ can be just as safe as Rust. The latter is just easier to use, especially in terms of working as a team of developers insomuch as the safety-nets are by default in place. Once again, C++ is powerful but comes with just as many footguns as C (in fact, more) and so requires a bit more finesse, experience, and skill to work with safely. Otherwise it can encapsulate pretty much any functionality you could possibly imagine, and that includes safety features. Checked memory accesses, managed memory, well-behaved arithmetic, all of that and more can be codified as a set of objects and interfaces. I started out in assembler, worked up to C, then finally on to C++ where I actually found it quite easy to write safe programs by just sticking to a strict set of guidelines. And that works. I have code that has been running almost continuously for over a decade without a hitch, and I credit that to good programming practices.
It isn't the safety or lack thereof, rather really adding another toolchain, development and debugging complexity.
Lets say I am doing something with V8, CLR, JVM, GCC, LLVM, CUDA, Metal, outside the managed languages that sit on top of those runtimes, compiler toolchains, GPGPU.
That infrastructure code is written in C++, and making use of Rust means adding yet another layer on how to compile, interop and debug, on top of what is already there.
As for safety in general, I am a believer of systems programming languages with automatic memory management, and polyglot programming, we aren't yet there because industry has the tendency to take decades to finally adopt good ideas, and only on a partial way.
Thus while I appreciate what Rust has achieved bringing affine types into mainstream, not an easy job, I consider this a transition step until we get both approaches in the same language, automatic resource management with affine,linear,effects,dependent types.
However this ultimately won't matter as much, when we eventually get our AI buddies good enough that they can spew executables directly.
674 comments
[ 3.7 ms ] story [ 360 ms ] threadhttps://www.twoscomplement.org
So no implicit type conversions, safer strings, etc.
> eventually I came to the depressing conclusion that there’s no way to get a group of C experts — even if they are knowledgable, intelligent, and otherwise reasonable — to agree on the Friendly C dialect. There are just too many variations, each with its own set of performance tradeoffs, for consensus to be possible.
Aliasing rules can also be problematic in some circumstances (but also beneficial for compiler optimisations).
And the orphan rule is also quite restrictive for adapting imported types, if you're coming from an interpreted language.
https://loglog.games/blog/leaving-rust-gamedev/ sums up the main issues nicely tbh.
I bet assembly programmers said the same about C!
Every language has relatively minor issues like these. Seriously pick a language and I can make a similar list. For C it will be a very long list!
It's important to be careful here: a lot (most? all?) of these rejections are programs that could be sound in a hypothetical Rust variant that didn't assert the unique/"noalias" nature of &mut reference, but are in fact unsound in actual Rust.
Anyhow, I won't go back to C++ land. Better this than whatever arcane, 1000-line, template-hell error message that kept me fed when I was there.
https://dlang.org/spec/betterc.html
A big hard to solve problem is you are likely using a C because of the ecosystem and/or the performance characteristics. Because of the C header/macro situation that becomes just a huge headache. All the sudden you can't bring in, say, boost because the header uses the quirks excluded from your smaller C language.
The core of Rust is actually very simple: Struct, Enum, Functions, Traits.
Sun actually did it right with Java, recognizing that if they mainly targeted SunOS/Solaris, no one would use it. And even though Oracle owns it now, it's not really feasible for them to make it proprietary.
Apple didn't care about other platforms (as usual) for quite a long time in Swift's history. Microsoft was for years actively hostile toward attempts to run .NET programs on platforms other than Windows. Regardless of Apple's or MS's current stance, I can't see myself ever bothering with Swift or C#/F#/etc. There are too many other great choices with broad platform and community support, that aren't closely tied to a corporation.
It's been 10 years. Even before that, no action was ever taken against Mono nor any restriction put or anything else. FWIW Swift shares a similar story, except Apple started to care only quite recently about it working anywhere else beyond their platforms.
Oh, and by the way, you need to look at these metrics: https://dotnet.microsoft.com/en-us/platform/telemetry
Maybe take off the conspiracy hat?
> There are too many other great choices with broad platform and community support
:) No, thanks, I'm good. You know why I stayed in .NET land and didn't switch to, say, Go? It's not that it's so good, it's because most alternatives are so bad in one or another area (often many at the same time).
* "No implicit type conversions" is trivial, and hardly worth mentioning. Trapping on both signed and unsigned overflow is viable but for hash-like code opting in to wrapping is important.
* "Safer strings" means completely different things to different people. Unfortunately, the need to support porting to the new language means there is little we can do by default, given the huge amount of existing code. We can however, add new string types that act relatively uniformly so that the code can be ported incrementally.
* For the particular case of arrays, remember that there are at least 3 different ways to compute its length (sentinel, size, end-pointer). All of these will need proper typing support. Particularly remember functions that take things like `(begin, middle end)`, or `(len, arr1[len], arr2[len])`.
* Support for nontrivial trailing array-or-other datums, and also other kinds of "multiple objects packed within a single allocation", is essential. Again, most attempted replacements fail badly.
* Unions, unfortunately, will require much fixing. Most only need a tag logic (or else replacement with bitcasting), but `sigval` and others like it are fundamentally global in nature.
* `va_list` is also essential to support since it is very widely used.
* The lack of proper C99 floating-point support, even in $CURRENTYEAR, means that compile-to-C implementations will not be able to support it properly either, even if the relevant operations are all properly defined in the new frontend to take an extra "rounding mode" argument. Note that the platform ABI matters here.
* There are quite a few things that macros are used for, but ultimately this probably is a finite set so should be possible to automatically convert with a SMOC.
Failure to provide a good porting story is the #1 mistake most new languages make.
Except for some missing pieces, this is safe and I have a prototype based on GCC that would warn about any unsafe features. va_list can be safely used at least with format strings and for union I need an annotations. Life times are the bigger outstanding issue.
What do you mean? What's wrong with floating point numbers in C99?
I'm far from an expert but I've seen enough to know it's wrong.
Safer strings is harder, as it gets into the general memory safety problem, but people have tried adding safer variants of all the classic functions, and warnings around them.
It's bad if it alters values (e.g. rounding). Promotion from one number representation to another (as long as it preserves values) isn't bad. This is trickier than it might seem, but Virgil has a good take on this (https://github.com/titzer/virgil/blob/master/doc/tutorial/Nu...). Essentially, it only implicitly promotes values in ways that don't lose numeric information and thus are always reversible.
In the example, Virgil won't let you pass "1000.00" to an integer argument, but will let you pass "100" to the double argument.
Depends on what you want from such a hierarchy, of course, but there is for example an injection i32 -> f64 (and if you consider the i32 operations to be undefined on overflow, then it’s also a homomorphism wrt addition and multiplication). For a more general view, various Schemes’ takes on the “numeric tower” are informative.
Yes, you can ask the std::sto* functions for the position where they stopped because of invalid characters and see if that position is the end of the string, but that is much more complex than should be needed for something like that.
These functions don't convert a string to a number, they try to extract a number from a string. I would argue that most of the time, that's not what you want. Or at least, most of the time it's not what I need.
atoi has the same problem of course, but even worse.
To get the equivalent of Rust's
you need something like: I find the choice to always require a start and and end position, and not to provide a method that simply passes or fails, to be quite baffling. In C++26, they also added an automatic boolean conversion for from_chars' return type to indicate success, which considers "only consumed half the input from the start" to be a success.Maybe I'm weird for mostly writing code that does straightforward input-to-number conversions and not partial string parsers, but I have yet to see a good alternative for Rust's parse().
https://doc.rust-lang.org/stable/rust-by-example/conversion/...
https://doc.rust-lang.org/stable/rust-by-example/conversion/...
If the conversion will always succeed (for example an 8-bit unsigned integer to a 32-bit unsigned integer), the From trait would be used to allow the conversion to feel implicit.
If the conversion could fail (for example a 32-bit unsigned integer to an 8-bit unsigned integer), the TryFrom trait would be used so that an appropriate error could be returned in the Result.
These traits prevent errors when converting between types and clearly mark conversions that might fail since they return Result instead of the output type.
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".
I like rust, but its not as clean in practice, as you describe
Then implement the `From` trait to map errors from third-party libraries to your own custom Error space:
Now you can convert any result into a single type that you control by transforming the errors: There is a little boilerplate and mapping between error spaces that is required but I don't find it that onerous.I would rather have what OCaml has: https://ocaml.org/docs/error-handling.
I actually find Zig quite a pleasant language other than my gripe with not handling more complex errors as cleanly.
Using out parameters for context, or diagnostic pattern like std does is not bad at all imo.
The only nice thing error payloads give you is better error messages in case of uncaught errors.
So, while this is an improvement over C++ (and that is not saying much at all), it's still implemented in a pretty clumsy way.
Combined with futures::try_join_all for async closures and you can use it to do a bunch of failable tasks in parallel too, it’s great.
If you use `anyhow`, then all you know is that the function may `Err`, but you do not know how - this is no better than calling a function that may `throw` any kind of `Throwable`. Not saying it's bad, it is just not that much different from the error handling in Kotlin or C#.
Whereas going with "I probably want to retry a few times" is guessing that most of your problems are the common case, but you're not entirely sure the platform you're on will emit non-commoncases with sane semantics.
Better than C, sufficient in most cases if you're writing an app, to be avoided if you're writing a lib. There are alternatives such as `snafu` or `thiserror` that are better if you need to actually catch the error.
Initial proof of concepts just get panics (usually with a message).
Then functions start to be fallible, by adding anyhow & considering all errors to still be fatal, but at least nicely report backtraces (or other things! context doesn't have to just be a message)
Then if a project is around long enough, swap anyhow to thiserror to express what failure modes a function has.
Doing error handling properly is hard, but it's a lot harder when error types lose information (integer/bool returns) or you can't really tell what errors you might get (exceptions, except for checked exceptions which have their own issues).
Sometimes error handling comes down to "tell the user", where all that info is not ideal. It's too verbose, and that's when you need anyhow.
In other cases where you need details, anyhow is terrible. Instead you want something like thiserror, or just roll your own error type. Then you keep a lot more information, which might allow for better handling. (HttpError or IoError - try a different server? ParseError - maybe a different parse format? etc.)
So I'm not sure it's that Result is clumsy, so much that there are a lot of ways to handle errors. So you have to pick a library to match your use case. That seems acceptable to me?
FWIW, errors not propagating via `?` is entirely a problem on the error type being propagated to. And `?` in closures does work, occasionally with some type annotating required.
As you say, it’s not “batteries included”. I think that’s a fine answer given rust is a systems language. But in application code I want batteries to be included. I don’t want to need to opt in to the right 3rd party library.
I think rust could learn a thing or two from Swift here. Swift’s equivalent is better thought through. Result is more part of the language, and less just bolted on:
https://docs.swift.org/swift-book/documentation/the-swift-pr...
[1] https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
[2] https://doc.rust-lang.org/std/panic/fn.set_hook.html
[3] https://doc.rust-lang.org/nomicon/panic-handler.html
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.
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
https://github.com/facebook/folly/commit/4d932ecc53c7a722671...
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.
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...
This is one odd the major reasons I switched to rust, just to escape spending my whole life worrying about bugs caused by UB.
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.
(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.
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).
Most users will do this:
1. Check if there is a value
2. Get the value
There is nothing theoretically preventing the compiler from enforcing that step 1 happens before step 2, especially if the compiler is able to combine the control flow branch with the process of conditionally getting the value. The practical issue is that there's no way to express this in C++ at all. The best you can do is the visitor pattern, which has horrible ergonomics and you can only hope it doesn't cause worse code generation too.
Some users want to do this:
1. Grab the value without checking to see if it's valid. They are sure it will be valid and can't or don't want to eat the cost of checking.
There is nothing theoretically preventing this from existing as a separate method.
I'm not a rust fanboy (seriously, check my GitHub @jchv and look at how much Rust I write, it's approximately zero) but Rust has this solved six ways through Sunday. It can do both of these cases just fine. The only caveat is that you have to wrap the latter case in an unsafe, but either way, you're not eating any costs you don't want to.
C++ can do this too. C++ has an active proposal for a feature that can fix this problem and make much more ergonomic std::variant possible, too.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p26...
Of course, this is one single microcosm in the storied history of C++ failing to adequately address the problem of undefined behavior proliferating the language, so I don't have high hopes.
Yeah I agree that sane defaults would be nice and that things like unchecked accesses should be more explicit. Having linear types to properly enforce issues with things like use-after-move would also be awesome. I don’t know that anyone has ever accused C++ of being particularly ergonomic. Sum types and pattern matching would be awesome.
Nothing wrong with being a Rust fanboy :), I have written some (https://github.com/afnanenayet/diffsitter).
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
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 ...
Divide by zero must be undefined behavior in any performant language. On x86 you either have a if before running the divide (which of course in some cases the compiler can optimize out, but only if it can determine the value is not zero); or you the CPU will trap into the OS - different OSes handle this in different ways, but most not in a while that makes it possible to figure out where you were and thus do something about it. This just came up in the C++ std-proposals mailing list in the past couple weeks.
AFAIK all common CPUs have the same behavior on integer overflow (two-complement). However in almost all cases (again, some encryption code is an exception) that behavior is useless to real code and so if it happens your code has a bug either way. Thus we may as well let compilers optimize assuming it cannot happen as it if it does you have a bug no matter what we define it as. (C++ is used on CPUs that are not two-complement as well, but we could call this implementation defined or unspecified, but it doesn't change that you have a bug if you invoke it.)
For std::expected - new benchmarks are proving in the real world, and with optimized exception handlers that exceptions are faster in the real world than systems that use things like expected. Microbenchmarks that show exceptions are slower are easy to create, but real world exceptions that unwind more than a couple function calls show different results.
As for modules, support is finally here and early adopters are using it. The road was long, but it is finally proving it worked.
Long roads are a good thing. C++ has avoided a lot of bad designs by spending a lot of time thinking about problems about things for a long time. Details often matter and move fast languages tend to run into problems when something doesn't work as well as they want. I'm glad C++ standardization is slow - it already is a mess without add more half backed features to the language.
I mean look, I already agree that it's not necessarily unreasonable to have undefined behavior, but this statement is purely false. You absolutely can eat your cake and have it too. Here's how:
- Split the operation in two: safe, checked division, and fast, unchecked division.
- OR, Stronger typing; a "not-zero" type that represents a numeric type where you can guarantee the value isn't zero. If you can't eat the cost of runtime checks, you can unsafely cast to this.
I think the former is a good fit for C++.
C++ does not have to do what Rust does, but for sake of argument, let's talk about it. What Rust does here is simple, it just defines divide-by-zero to panic. How? Multiple ways:
- If it knows statically it will panic, that's a compilation error.
- If it knows statically it can not be zero, it generates unchecked division.
- If it does not know statically, it generates a branch. (Though it is free to implement this however it wants; could be done using CPU exceptions/traps if they wanted.)
What if you really do need "unsafe" division? Well, that is possible, with unchecked_div. Most people do not need unchecked_div. If you think you do but you haven't benchmarked yet, you do not. It doesn't get any simpler than that. This is especially the case if you're working on modern CPUs with massive pipelines and branch predictors; a lot of these checks wind up having a very close to zero cost.
> AFAIK all common CPUs have the same behavior on integer overflow (two-complement). However in almost all cases (again, some encryption code is an exception) that behavior is useless to real code and so if it happens your code has a bug either way. Thus we may as well let compilers optimize assuming it cannot happen as it if it does you have a bug no matter what we define it as. (C++ is used on CPUs that are not two-complement as well, but we could call this implementation defined or unspecified, but it doesn't change that you have a bug if you invoke it.)
It would be better to just do checked arithmetic by default; the compiler can often statically eliminate the checks, you can opt out of them if you need performance and know what you're doing, and the cost of checks is unlikely to be noticed on modern processors.
It doesn't matter that this usually isn't a problem. It only has to be a problem once to cause a serious CVE. (Spoiler alert: it has happened more than once.)
> For std::expected - new benchmarks are proving in the real world, and with optimized exception handlers that exceptions are faster in the real world than systems that use things like expected. Microbenchmarks that show exceptions are slower are easy to create, but real world exceptions that unwind more than a couple function calls show different results.
You can always use stack unwinding or exceptions if you want to; that's also present in Rust too, in the form of panic. The nice thing about something like std::expected is that it theoretically can bridge the gap between code that uses exceptions and code that doesn't: you can catch an exception and stuff it into the `e` of an std::expected value, or you can take the `e` value of an std::expected and throw it. In theory this should not have much higher cost than simply throwing.
> As for modules, support is finally here and early adopters are using it. The road was long, but it is finally proving it worked.
Last I was at Google, th...
There is no reason why make could not work with modules if someone wanted to go through the effort. The CMake people have even outlined what needs to be done. Ninja is so much nicer that you should switch anyway - I did more than 10 years ago.
Anyway, the Google C++ style guide has nothing to do with why C++ modules aren't and won't be used at Google, it's because as-implemented modules are not an obvious win. They can theoretically improve performance, but they can and do also make some cases worse than before.
I don't think most organizations will adopt modules at this rate. I suspect the early adopters will wind up being the only adopters for this one.
This attitude is why modern software is dogshit slow. People make this "if you haven't benchmarked, it doesn't matter" argument thousands of times and the result is that every program I run is slower than molasses in Siberia. I don't care about "safety" at the expense of performance.
Bullshit. Here's my proof: We don't even do this. There's a ton of software that isn't safe from undefined behavior and it's still slow as shit.
> People make this "if you haven't benchmarked, it doesn't matter" argument thousands of times and the result is that every program I run is slower than molasses in Siberia. I don't care about "safety" at the expense of performance.
If you can't imagine a world where there's nuance between "we should occasionally eat 0.5-1ns on checking an unsafe division" and "We should ship an entire web browser with every text editor and chat app" the problem is with you. If you want your software to be fast, it has to be benchmarked, just like if you want it to be stable, it has to be tested. There's really no exceptions here, you can't just guess these things.
Checked arithmetic is plenty fast. And as for safety vs performance, quickly computing the incorrect result is strictly less useful than slowly computing the correct one.
Why not make division by zero implementation defined. I'm happy with my compiler telling me my program will get terminated if I divide by zero, no problem. Let's even say it "may" be terminated (because maybe the division is optimised out if we don't actually need to calculate it, fine).
My problem is that UB let's compilers do all kinds of weird things, like assume if I write:
Then set dividebyzero to always be 0, because 'obviously' y can't be 0, because then I would invoke undefined behaviour.Also, for two-complement, it's fairly common people want wrapping behaviour. Also, I don't think basically anyone is using C++ on non-twos complement CPUs (both gcc and clang don't support it), and even if it does run on such CPUs, why not still require a well-defined behaviour, in the same way C++ runs on 32-bit and 64-bit systems, but we don't say asking for the size of a pointer is undefined behaviour -- everyone just defines what it is on their system!
> 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 ...
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.
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.
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.
However it seems like C++ wants to only provide this kind of pattern via monadic operations.
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.
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").
edit: what's with people downvoting a straight fact?
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.
> 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.
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.
Who are you speaking to who hasn't explored all those things in depth?
I see Rust's restrictions as a huge advantage over C++ here. Even with respect to editions. Rust has always given me the impression of a language designed from the start to be approximately what C++ is today, without the cruft, in which safety is opt-out, not opt-in. And the restrictions seem more likely to preserve that than not.
C/C++ folks seem to see Rust's restrictions as anti-features without realizing that C/C++'s lack of restriction resulted in the situation they have today.
I only maintain a few projects in each language, so I haven't run into every sort of issue for either, but that's very much how it feels to me still, several years and several projects in.
I agree that Rust is designed to be like C++ is today, without the cruft, except all languages if they survive long enough in the market, beyond the adoption curve, they will eventually get their own cruft.
Not realizing this, will only make that 30 years from now, if current languages haven't yet been fully replaced by AI based tools, there will be that language designed to be like Rust is in 30 years, but without the cruft.
The strength of C++ code today is on the ecosystem, that is why we reach for it, having to write CUDA, DirectX, maybe dive into the innards of Java, CLR, V8, GCC, LLVM, doing HPC with OpenAAC, OpenMP, MPI, Metal, Unreal, Godot, Unity.
Likewise I don't reach for C for fun, the less the merrier, rather POSIX, OpenGL, Vulkan,....
Well I'm not them. I'm just a regular old software developer.
> The strength of C++ code today is on the ecosystem
Ecosystem is why I jumped ship from C++ to Rust. The difference in difficulty integrating a random library into my project is night and day. What might take a week or a month in C++ (integrating disparate build systems, establishing types and lifetimes of library objects and function calls, etc) takes me 20 minutes in Rust. And in general I find the libraries to be much smaller, more modular, and easier to consume piecemeal rather than a whole BOOST or QT at a time.
And while the Rust libraries are younger, I find them to be more stable, and often more featureful and with better code coverage. The language seems to lend itself to completionism.
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.
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.
Meaning that all the machines I've ever cared about have had 8 bit bytes. The TI-99/4A, TRS-80, Commodore 64 and 128, Tandy 1000 8088, Apple ][, Macintosh Classic, etc.
Many were launched in the late 70s. By 1985 we were well into the era of PC compatibles.
My https://en.wikipedia.org/wiki/Tandy_1000 came out in 1984. And it was a relatively late entry to the market, it was near peak 8088 with what was considered high end graphics and sound for the day, far better than the IBM PC which debuted in 1981 and only lasted until 1987.
0. https://en.wikipedia.org/wiki/Floating-point_unit
1. https://www.tindie.com/products/jurassicomp/68882-fpu-card-f...
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".
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.
This isn't really true since Rust has panics. It would be nice to have out-of-the-box support for a "no panics" subset of Rust, which would also make it easier to properly support linear (no auto-drop) types.
not sure what the latest is in the space, if I recall there are some subtleties
It would be annoying to use - as you say, you couldn’t even add regular numbers together or index into an array in nopanic code. But there are ways to work around it (like the wrapping types).
One problem is that implicit nopanic would add a new way to break semver compatibility in APIs. Eg, imagine a public api that just happens to not be able to panic. If the code is changed subtly, it could easily start panicing again. That could break callers, so it has to be a major version bump. You’d probably have to require explicit nopanic at api boundaries. (Else assume all public functions from other crates can panic). And because of that, public APIs like std would need to be plastered with nopanic markers everywhere. It’s also not clear how that works through trait impls.
As far as I can tell, no_std doesn't change anything with regard to either the usability of panicking operators like integer division, slice indexing, etc. (they're still usable) nor on whether they panic on invalid input (they still do).
Also there is the no_panic crate, which uses macros to require the compiler to prove that a given function cannot panic.
But on those places, you better know exactly what you are doing.
You see this whenever you use cargo test. If a single test panics, it doesn’t abort the whole program. The panic is “caught”. It still runs all the other tests and reports the failure.
Although as a library vendor, you kind have to assume your library could be compiled into an app configured with panic=abort, in which case it will not do that
Even then, though, I do see a need to catch panics in some situations: if I'm writing some sort of API or web service, and there's some inconsistency in a particular request (even if it's because of a bug I've written), I probably really would prefer only that request to abort, not for the entire process to be torn down, terminating any other in-flight requests that might be just fine.
But otherwise, you really should just not be catching panics at all.
you could have a panic though, if you wrongly make assumptions
I will fight against program aborts as hard as I possibly can. I don't mind boilerplate to be the price paid and will provide detailed error messages even in such obscure error branches.
Again, speaking only for myself. My philosophy is: the program is no good for me dead.
That may be true, but the program may actually be bad for you if it does something unexpected due to an unforeseen state.
When I have to make a decision on an app-level, it becomes a different game though. I don't have a clear-and-cut answer for that.
Usually I just use the `?` and `.map_err` (or `anyhow` / `thiserror`) to delegate and move on with life.
I have a few places where I do pattern-matches to avoid exactly what you described: imposing the extra internal complexity to users. Which is indeed a bad thing and I am trying to fight it. Not always succeeding.
Panics are a runtime memory safe way to encode an invariant, but I will generally prefer a compile time invariant if possible and not too cumbersome.
However, yes I will panic if I'm not already using unsafe and I can clearly prove the invariant I'm working with.
Panic is absolutely fine for bugs, and it's indeed what should happen when code is buggy. That's because buggy code can make absolutely no guarantees on whether it is okay to continue (arbitrary data structures may be corrupted for instance)
Indeed it's hard to "treat an error" when the error means code is buggy. Because you can rarely do anything meaningful about that.
This is of course a problem for code that can't be interrupted.. which include the Linux kernel (they note the bug, but continue anyway) and embedded systems.
Note that if panic=unwind you have the opportunity to catch the panic. This is usually done by systems that process multiple unrelated requests in the same program: in this case it's okay if only one such request will be aborted (in HTTP, it would return a 5xx error), provided you manually verify that no data structure shared by requests would possibly get corrupted. If you do one thread per request, Rust does this automatically; if you have a smaller threadpool with an async runtime, then the runtime need to catch panics for this to work.
And now your language has exceptions - which break control flow and make reasoning about a program very difficult - and hard to optimize for a compiler.
And that's why my programs get compiled with panic=abort, that makes panics just quit the program, with no ability to catch them, and no programs in zombie states where some threads panicked and others keep going on.
But see, catch_panic is an escape hatch. It's not meant to be used as a general error handling mechanism and even when doing FFI, Rust code typically converts exceptions in other languages into Results (at a performance cost, but who cares). But Rust needs a escape right, it is a low level language.
And there is at least one case where the catch_unwind is fully warranted: when you have an async web server with multiple concurrent requests and you need panics to take down only a single request, and not the whole server (that would be a DoS vector). If that weren't possible, then async Rust couldn't have feature parity with sync Rust (which uses a thread-per-request model, and where panics kill the thread corresponding to the request)
Addressed in sibling thread - it’s a poor default to design Rust around.
Unfortunately even the Rust core language doesn't treat them this way.
I think it's arguably the single biggest design mistake in the Rust language. It prevents a ton of useful stuff like temporarily moving out of mutable references.
They've done a shockingly good job with the language overall, but this is definitely a wart.
This is a sign you are writing an operating system instead of using one. Your web server should be handling requests from a pool of processes - so that you get real memory isolation and can crash when there is a problem.
If there was a special case that would not work, then the design dictates that requests are not independent and there must be risk of interference (they are in the same process!)
What I definitely do not want is a bug ridden “crashable async sub task” system built in my web program.
you’re vastly over estimating the overhead of processes and number of simultaneous web connections.
> only to gain a minor amount of safety
What you’re telling me is performance (memory?) is such a high priority you’re willing to make correctness and security tradeoffs.
And I’m saying thats ok, one of those is crashing might bring down more than one request.
> one that is virtually irrelevant in a memory safe language
Your memory safe language uses C libraries in its process.
Memory safe languages have bugs all the time. The attack surface is every line of your program and runtime.
Memory is only one kind of resource and privilege. Process isolation is key for managing resource access - for example file descriptors.
Chrome is a case study if these principles. Everybody thought isolating JS and HTML pages should be easy - nobody could get it right and chrome instead wrapped each page in a process.
It's less the actual overhead of the process but the savings you get from sharing. You can reuse database connections, have in-memory caches, in-memory rate limits and various other things. You can use shared memory which is very difficult to manage or an additional common process, but either way you are effectively back to square one with regards to shared state that can be corrupted.
I just said one of the costs of those saving is crashing may bring down multiple requests - and you should design with that trade off.
Handling thousands of concurrent requests is table stakes for a simple web server. Handling thousands of concurrent processes is beyond most OSs. The context switching overhead alone would consume much of the CPU of the system. Even hundreds of processes will mean a good fraction of the CPU being spent solely on context switching - which is a terrible place to be.
It works fine on Linux - the operating system for the internet. Have you tried it?
> good fraction of the CPU being spent solely on context switching
I was waiting for this one. Threads and processes do the same amount of context switching. The overhead of processes switch is a little higher. The main cost is memory.
Yes, therefore real webservers use a limited amount of threads/processes (in the same ballpark as a number of CPU cores). Modern approach is to use green threads which are really cheap to switch, it is like store registers, read registers and jmp.
> The main cost is memory.
The main cost is scheduling, not switching per se. Preemptive multitasking needs to deal with priorities to not waste time, and algorithms that do it are O(N) mostly. All these O(N) calculations needs to be completed multiple times per second, the higher the frequency of switching the more work to do. When you have thousands of processes it is the main cost. If you have tens of thousands it starts to bite hard.
The person I am having a conversation with is advocating for threads instead of processes. How do you think threads work?
> Modern approach is to use green threads which are really cheap to switch, it is like store registers, read registers and jmp.
That’s certainly the popular approach. As I said at the beginning this approach is making a mini operating system with more bugs and less security rather than leveraging the capabilities of your operating system.
Once again, im waiting to here about your experience of maxing out processes and after that having to switch to green threads.
Are they? I looked back and I've found this quote of them: "The overhead of process-per-request, or even thread-per-request, is absurd if you're already using a memory safe language." Doesn't seem as an advocacy for thread-per-request to me.
> As I said at the beginning this approach is making a mini operating system with more bugs and less security rather than leveraging the capabilities of your operating system.
Lets look at Apache for example. It starts a few processes and/or threads, but then each thread deals with a lot of connections. The threads Apache starts are for spreading work over several CPUs and maybe to overcome some limits of select/poll/epoll. The main approach is to track a state of a connection, and when something happens on a socket, Apache find the state of the connection and deals with events on the socket. Then it stores the new state and moves to deal with other sockets in the same manner.
It is like green threads but without green threads. Green threads streamlines all this state keeping by allowing each connection to have it's own stack. And I'd say it is easier to do right than to write a finite automata for HTTP/HTTPS.
> Once again, im waiting to here about your experience of maxing out processes and after that having to switch to green threads.
Oh, I didn't. A long long time ago I was reading stuff on networking. All of it was in one opinion: 10k kernel tasks maybe a tolerable solution, but 100k is bad. IIRC Apache had a document describing its internal architecture and explaining why it is as it is.
So I wouldn't even try to start thousands of threads. I mean I tried to start 1000s of processes when I was young and learned about fork-bombs, and this experience confirmed it for me, that 1000s of processes is not a really good idea.
Moreover I completely agree with them: if you use a memory-safe language, then it is strange to pay costs for preemptive multitasking just to have separate virtual address spaces. I mean, it will be better to get a virtual machine with JIT compiler, and run code for different connection on different instances of a virtual machine. O(1) complexity of cooperative switching will beat O(N) complexity of preemptive switching. To my mind hardware memory management is overrated.
Apache has years of engineering work - and almost weekly patches to fix issues related to security. Many of these security issues would go away if they were not using special technique to optimize performance.
But the best part of the web is its modular. So now your application doesn’t need to that. It can leverage those benefits without complexity cascade.
For example, Apache can manage more connections than your application needs running processes for.
> I was reading stuff on networking….
That’s exactly my point. Too many people are repeating advice from Google or Facebook and not actually thinking about real problems they face.
Can you serve more requests using specialized task management? Yes. You can make a mini-OS with fewer features to squeeze out more scheduling performance and that’s what some big companies did.
But you will pay for that with reduced security and reliability. To bring it back to my original complaint - you must accept that a crash can bring down multiple requests.
And it’s an insane default to design Rust around. It’s especially confusing to make all these arguments about how “unsafe” languages are, but then ignore OS safety in hopes of squeezing out a little more perf.
> So I wouldn't even try to start thousands of threads.
Please try it before arguing it doesn’t work. Fork bombing is recursive and unrelated.
> if you use a memory-safe language, then it is strange to pay costs for preemptive multitasking just to have separate virtual address spaces
Then why do these “memory-safe” languages need constant security patches? Why does chrome need to wrap each page’s JS in its own process?
In theory you’re right. If they are actually memory-safe then you don’t need to consider address spaces. But in practice the attack surface is massive and processes give you stronger invariants.
I was certainly not, I explicitly said that thread-per-request is as bad as process-per-request. I could even agree that it's the worse of both worlds to some extent - none of the isolation, almost all of the overhead (except if you're using a language with a heavy runtime, like Java, where spawning a new JVM has a huge cost compared to a new thread in an existing JVM).
Modern operating systems provide many mechanisms for doing async IO specifically to prevent the need for spawning and switching between thousands of processes. Linux in particular has invested heavily in this, from select, to poll, to epoll, and now unto io_uring.
OS process schedulers are really a poor tool for doing massively parallel IO. They are a general purpose algorithm that has to keep in mind many possible types of heterogeneous processes, and has no insight into the plausible behaviors of those. For a constrained problem like parallel IO, it's a much better idea to use a purpose-built algorithm and tool. And they have simply not been optimized with this kind of scale in mind, because it's much more important and common use case to run quickly for a small number of processes than it is to scale up to thousands. There's a reason typical ulimit configurations are limited to around 1000 threads/processes per system for all common distros.
Correction. People who wanted to do async IO went and added additional support for it. The primary driver is node.js.
> And they have simply not been optimized with this kind of scale in mind,
yes, processes do not sacrifice security and reliability. That’s the difference.
The fallacy here is assuming that a process is just worse for hand wavy reasons and that your language feature has fa secret sauce.
If it’s not context switching then that means you have other scheduling problems because you cannot be pre-empted.
> There's a reason typical ulimit configurations are limited to around 1000 threads/processes per system
STILL waiting to hear about your experience of maxing out Linux processes on a web server - and then fixing it with green threads.
I suspect it hasn’t happened.
If you want a less flippant take down of this idea and a more complete description of my position, please see: https://burntsushi.net/unwrap/
> Honestly, I don't think libraries should ever panic. Just return an UnspecifiedError with some sort of string.
The latter is not a solution to the former. The latter is a solution to libraries having panicking branches. But panics or other logically incorrect behavior can still occur as a result of bugs.
Panicking should always be treated as a bug. They are assertions.
Assuming that you are not using much recursion, you can eliminate most of the heap related memory panics by adding limited reservation checks for dynamic data, which is allocated based on user input/external data. You should also use statically sized types whennever possible. They are also faster.
But for arithmetics Rust has non-aborting bound checking API, if my memory serves.
And that's what I'm trying hard to do in my Rust code f.ex. don't frivolously use `unwrap` or `expect`, ever. And just generally try hard to never use an API that can crash. You can write a few error branches that might never get triggered. It's not the end of the world.
Of course, just like with opening files or integer arithmetic, if you don't pay any attention to handling the errors up front when writing your code, it can be an onerous if not impossible to task to refactor things after the fact.
I was approaching these problems strictly from the point of view of what can Rust do today really, nothing else. To me having checked and non-panicking API for integer overflows / underflows at least gives you some agency.
If you don't have memory, well, usually you are cooked. Though one area where Rust can become even better there is to give us some API to reserve more memory upfront, maybe? Or I don't know, maybe adopt some of the memory-arena crates in stdlib.
But yeah, agreed. Not the types of problems I want to have anymore (because I did have them in the past).
Rust also provides Wrapping and Saturating wrapper types for these integers, which wrap (255 + 1 == 0) or saturate (255 + 1 == 255). Depending on your CPU either or both of these might just be "how the computer works anyway" and will accordingly be very fast. Neither of them is how humans normally think about arithmetic.
Furthermore, Rust also provides operations which do all of the above, as well as the more fundamental "with carry" type operations where you get two results from the operation and must write your algorithms accordingly, and explicitly fallible operations where if you would overflow your operation reports that it did not succeed.
Return an AllocationError. Rust unfortunately picked the wrong default here for the sake of convenience, along with the default of assuming a global allocator. It's now trying to add in explicit allocators and allocation failure handling (A:Allocator type param) at the cost of splitting the ecosystem (all third-party code, including parts of libstd itself like std::io::Read::read_to_end, only work with A=GlobalAlloc).
Zig for example does it right by having explicit allocators from the start, plus good support for having the allocator outside the type (ArrayList vs ArrayListUnmanaged) so that multiple values within a composite type can all use the same allocator.
>Also many functions use addition and what is one supposed to do in case of overflow?
Return an error ( https://doc.rust-lang.org/stable/std/primitive.i64.html#meth... ) or a signal that overflow occurred ( https://doc.rust-lang.org/stable/std/primitive.i64.html#meth... ). Or use wrapping addition ( https://doc.rust-lang.org/stable/std/primitive.i64.html#meth... ) if that was intended.
Note that for the checked case, it is possible to have a newtype wrapper that impls std::ops::Add etc, so that you can continue using the compact `+` etc instead of the cumbersome `.checked_add(...)` etc. For the wrapping case libstd already has such a newtype: std::num::Wrapping.
Also, there is a clippy lint for disallowing `+` etc ( https://rust-lang.github.io/rust-clippy/master/index.html#ar... ), though I assume only the most masochistic people enable it. I actually tried to enable it once for some parsing code where I wanted to enforce checked arithmetic, but it pointlessly triggered on my Checked wrapper (as described in the previous paragraph) so I ended up disabling it.
I partially disagree with this. Using Zig style allocators doesn't really fit with Rust ergonomics, as it would require pretty extensive lifetime annotations. With no_std, you absolutely can roll your own allocation styles, at the price of more manual lifetime annotations.
I do hope though that some library comes along that allows for Zig style collections, with the associated lifetimes... (It's been a bit painful rolling my own local allocator for audio processing).
As long as the type is generic on the allocator, the lifetimes of the allocator don't appear in the type. So eg if your allocator is using a stack array in main then your allocator happens to be backed by `&'a [MaybeUninit<u8>]`, but things like Vec<T, A> instantiated with A = YourAllocator<'a> don't need to be concerned with 'a themselves.
Eg: https://play.rust-lang.org/?version=nightly&mode=debug&editi... do_something_with doesn't need to have any lifetimes from the allocator.
If by Zig-style allocators you specifically mean type-erased allocators, as a way to not have to parameterize everything on A:Allocator, then yes the equivalent in Rust would be a &'a dyn Allocator that has an infectious 'a lifetime parameter instead. Given the choice between an infectious type parameter and infectious lifetime parameter I'd take the former.
I guess all that to say, I agree then that this should've been in std from day one.
Odin has them, too, optionally (and usually).
Rust picked the right default for applications that run in an OS whereas Zig picked the right default for embedded. Both are good for their respective domains, neither is good at both domains. Zig's choice is verbose and useless on a typical desktop OS, especially with overcommit, whereas Rust's choice is problematic for embedded where things just work differently.
My current $dayjob involves a "server" application that needs to run in a strict memory limit. We had to write our own allocator and collections because the default ones' insistence on using GlobalAlloc infallibly doesn't work for us.
Thinking that only "embedded" cares about custom allocators is just naive.
As a video game developer, I've found the case for custom general-purpose allocators pretty weak in practice. It's exceedingly rare that you really want complicated nonlinear data structures, such as hash maps, to use a bump-allocator. One rehash and your fixed size arena blows up completely.
95% of use cases are covered by reusing flat data structures (`Vec`, `BinaryHeap`, etc.) between frames.
Requests are matched against the smallest tier that can satisfy them (static tiers before dynamic). If no tier can satisfy it (static tiers are too small or empty, dynamic tier's "remaining" count is too low), then that's an allocation failure and handled by the caller accordingly. Eg if the request was for the initial buffer for accepting a client connection, the client is disconnected.
When a buffer is returned to the allocator it's matched up to the tier it came from - if it came from a static tier it's placed back in that tier's list, if it came from the dynamic tier it's free()d and the tier's used counter is decremented.
Buffers have a simple API similar to the bytes crate - "owned buffers" allow &mut access, "shared buffers" provide only & access and cloning them just increments a refcount, owned buffers can be split into smaller owned buffers or frozen into shared buffers, etc.
The allocator also has an API to query its usage as an aggregate percentage, which can be used to do things like proactively perform backpressure on new connections (reject them and let them retry later or connect to a different server) when the pool is above a threshold while continuing to service existing connections without a threshold.
The allocator can also be configured to allocate using `mmap(tempfile)` instead of malloc, because some parts of the server store small, infrequently-used data, so they can take the hit of storing their data "on disk", ie paged out of RAM, to leave RAM available for everything else. (We can't rely on the presence of a swapfile so there's no guarantee that regular memory will be able to be paged out.)
As for crates.io, there is no option. We need local allocators because different parts of the server use different instances of the above allocator with different tier configs. Stable Rust only supports replacing GlobalAlloc; everything to do with local allocators is unstable, and we don't intend to switch to nightly just for this. Also FWIW our allocator has both a sync and async API for allocation (some of the allocator instances are expected to run at capacity most of the time, so async allocation with a timeout provides some slack and backpressure as opposed to rejecting requests synchronously and causing churn), so it won't completely line up with std::alloc::Allocator even if/when that does get stabilized. (But the async allocation is used in a localized part of the server so we might consider having both an Allocator impl and the async direct API.)
And so because we need local allocators, we had to write our own replacements of Vec, Queue, Box, Arc, etc because the API for using custom A with them is also unstable.
Who writes the crates?
I said absolutely no such thing? In my $dayjob working on graphics I, too, have used custom allocators for various things, primarily in C++ though, not Rust. But that in no way makes the default of a global allocator wrong, and often those custom allocators have specialized constraints that you can exploit with custom containers, too, so it's not like you'd be reaching for the stdlib versions probably anyway.
Going from panic to panic free in Rust is as simple as choosing 'function' vs 'try_function'. The actual mistakes in Rust were the ones where the non-try version should have produced a panic by default. Adding Box::try_new next to Box::new is easy.
There are only two major applications of panic free code in Rust: critical sections inside mutexes and unsafe code (because panic safety is harder to write than panic free code). In almost every other case it is far more fruitful to use fuzzing and model checking to explicitly look for panics.
Well on Linux they are apparently supposed to return memory anyway and at some point in the future possibly SEGV your process when you happen to dereference some unrelated pointer.
They require overcommit just to open an empty window.
Honestly this is where you'd throw an exception. It's a shame Rust refuses to have them, they are absolutely perfect for things like this...
The only place where it would be different is if you explicitly set panics to abort instead of unwind, but that's not default behavior.
You abandon the current activity and bubble up the error to a stage where that effort can be tossed out or retried sometime later. i.e. Use the same error handling approach you would have to use for any other unreliable operation like networking.
You can configure your lints in your workspace-level Cargo.toml (the folder of crates)
“””
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -1 }
# arithmetic_side_effects = "deny"
unwrap_used = "deny"
expect_used = "deny"
panic = "deny"
“””
then in your crate Cargo.toml “””
[lints]
workspace = true
“””
Then you can’t even compile the code without proper error handling. Combine that with thiserror or anyhow with the backtrace feature and you can yeet errors with “?” operators or match on em, map_err, map_or_else, ignore them, etc
[1] https://rust-lang.github.io/rust-clippy/master/index.html#un...
> Text after a blank line that is indented by two or more spaces is reproduced verbatim. (This is intended for code.)
https://rust-lang.github.io/rust-clippy/master/#indexing_sli...
Not saying there aren't applications where using these lints couldn't be alright (web servers maybe), but at least in my experiences (mostly doing CLI, graphics, and embedded stuff) trying to keep the program alive leads to more problems than less.
It's totally normal practice for a library to have this as a standard.
I could of course create my own type for this, but then it won’t work with the ? operator.
Just need a function that allows lifting option to result.
This is what the Try[^1] trait is aiming to solve, but it's not stabilized yet.
[^1]: https://rust-lang.github.io/rfcs/3058-try-trait-v2.html
I could imagine situations where an empty return value would constitute an Error, but in 99% of cases returning None would be better.
Result<Option> may feel clunky, but if I can give one recommendation when it comes to Rust, is that you should not value your own code-aesthetical feelings too much as it will lead to a lot of pain in many cases — work with the grain of the language not against it even if the result does not satisfy you. In this case I'd highly recommend just using Result<Option> and stop worrying about it.
You being able to compose/nest those base types and unwraping or matching them in different sections of your code is a strength not a weakness.
But I hear compiling is too slow.
Is it a serious problem in practice?
Is it a serious problem? I'd say 'no', but YMMV.
Besides developer productivity it can be an issue when you need a critical fix to go out quickly and your pipelines take 60+ minutes.
With some exceptions for core data structures, it seems that if you only modified a few files in a large project the total compilation time would be quick no matter how slow the compiler was.
Rust compile times have been improving over time as the compiler gets incrementally rewritten and optimised.
With many other build systems I'd be hesitant to do that, but since Cargo is very good about what to rebuild for incremental builds, keeping the cache around is a huge speed boost.
Fast ability to quickly test and get feedback is mana from the gods in software development. Organizations should keep it right below customer satisfaction and growth as a driving metric.
Granted, there aren't any Rust projects that large yet, but I feel like compilation speeds are something that can be worked around with tooling (distributed build farms, etc.). C++'s lack of safety and a proclivity for "use after free" errors is harder to fix.
Like with any bigger C++ project there's like 3 build tools, two different packaging systems and likely one or even multiple code generators.
Cargo also has good caching out of the box. While cargo is not the best build system, it's an easy to use good system, so you generally get good compile times for development when you edit just one file. This is along made heavy use of with docker workflows like cargo-chef.
So if you're cool with C++ or Java compile times, Rust will generally be fine. If you're coming from Go, Rust compiles will fell positively glacial.
https://github.com/abseil/abseil-cpp/blob/master/absl/status...
In any case I will take Rust Result over C++ mess at any time especially given that we have two C++, one with exception support and one without making code incompatible between two.
I use completely custom error handling stacks in C++ and they are quite slick these days, thanks to improvements in the language.
With Rust Result and powerful macros it easier to implement.
Back at Google, it was truly an error handling nirvana because they had StatusOr which makes sure that the error type is just Status, a standardized company-wide type that stills allows significant custom errors that map to standardized error categories.
Why can't I return an integer on error? What's preventing me from writing Rust like C++?
For instance, a common example of the "integer on error" pattern in other languages is `array.index_of(element)`, returning a non-negative index if found or a negative value if not found. In Rust, the return type of `Iterator::position` is instead `Option<usize>`. You can't accidentally forget to check whether it's present. You could still write your own `index_of(&self, element: &T) -> isize /* negative if not found */` if that's your preference.
https://doc.rust-lang.org/std/iter/trait.Iterator.html#metho...
1: https://news.ycombinator.com/item?id=41543183
it can look just like a more-efficient `except` clauses with all the safety, clarity, and convenience that enums provide.
Here's an example:
* Implementing an error type with enums: https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/... * Which derives from a more general error type with even more helpful enums: https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/... * then some straightforward handling of the error: https://git.deuxfleurs.fr/Deuxfleurs/garage/src/branch/main/...
It's obviously subjective in many ways. However, what I dislike the most is that try/except hides the error path from me when I'm reading code. Decades of trying to figure out why that stacktrace is happening in production suddenly has given me a strong dislike for that path being hidden from me when I'm writing my code.
It could be some kind of an exception check thing, where you would either have to make sure that you handle the error locally somehow, or propagate it upwards. Sadly programming is not ready for such ideas yet.
---
I jest, but this is exactly what checked exceptions are for. And the irony of stuff like Rust's use of `Result<T, E>` and similarly ML-ey stuff is that in practice they end up with what are essentially just checked exceptions, except with the error type information being somewhere else.
Of course, people might argue that checked exceptions suck because they've seen the way Java has handled them, but like... that's Java. And I'm sorry, but Java isn't the definition of how checked exceptions can work. But because of Java having "tainted" the idea, it's not explored any further, because we instead just assume that it's bad by construction and then end up doing the same thing anyway, only slightly different.
The key phrase you're looking for is "algebraic effect systems". Right now they're a pretty esoteric thing only really seen in PL research, but at one point so was most of the stuff we now take for granted in Rust. Maybe someday they'll make their way to mainstream languages in an ergonomic way.
Nim has a good take on exception tracking that's elegant, performant, and only on request (unlike Java's attempt).
Where Java failed is the inability to write generic code that uses checked exceptions - e.g. a higher-order function should be able to say, "I take argument f, and I might throw anything that f() throws, plus E1". But that, as you rightly point out, is a Java problem, not a checked exception problem. In fact, one of the more advanced proposals for lambda functions in Java tackled this exact issue (but unfortunately they went with a simpler proposal that didn't).
Programmer's fault: runtime exception
Not programmer's fault: checked exception
Reading from a file but the disk fails? Not programmer's fault. IOException (checked). Missed a null somewhere? Programmer's fault. NullPointerException (unchecked).
Or, alternatively, the interface wouldn't declare it as thrown, and then you couldn't implement it in terms of disk I/O without rewrapping everything into unchecked exceptions. A good example of that is java.util.Map, if you try to implement it on top of a file-based value store.
That said, I'd prefer to be working in Rust. The C++ code we call into can just raise exceptions anywhere implicitly; there are a hell of a lot of things you can accidentally do wrong without warning; class/method syntax is excessively verbose, etc.
And those 'special' stdlib types wouldn't be half as useful without supporting language syntax, so why not go the full way and just implement everything in the language?
You might add syntactic sugar on top, but you don't want these kinds of things in your fundamental language definition.
after all, if a library exposes too many functions to you, it isn't a good library.
what good is it for me to have a result type if i have to call 27 functions with 27 different result types just to rotate a cube?
Unless you specifically want the ‘?’ operator, you can get pretty close to this with some clever use of templates and operator overloading.
If universal function call syntax becomes standardized, this will look even more functional and elegant.
I really wish java used `?` as a shorthand to declare and propagate checked exceptions of called function.
In my experience, a lot of the code is dedicated to "correctly transforming between different Result / Error types".
Much more verbose than exceptions, despite most of the time pretending they're just exceptions (i.e. the `?` operator).
Why not just implement exceptions instead?
(TBH I fully expect this comment to be downvoted, then Rust to implement exceptions in 10 years... Something similar happened when I suggested generics in Go.)
Being blind to the alternative, and mostly authoring lower level libraries, what's the benefit of not having exceptions? I understand how they're completely inappropriate for an OS, a realtime system, etc, but what about the rest? Or is that the problem: once you have the concept, you've polluted everything?
I wouldn't say that it's the tedious part of the language.
People often complain about:
- performance impact, either speed (most languages) or binary size (C++); this, however, is mostly an implementation concern, and doesn't impact Rust at all, as exceptions can simply be syntax-level compiler sugar, and can be compiled exactly the same as Result type is currently (having said that, the optional stack trace is another potential issue, which would have a performance impact even in Rust)
- checked exceptions - this is particularly a concern in Java, which has a fairly poor type system (closed subtyping only) and no type inference, so declaring all unhandled exceptions is tedious
- non-checked exceptions - in this case, "every exception can happen anywhere" so it's ostensibly unsafe (well it's just how life is, and even Java has special unchecked exceptions such as OutOfMemory that can happen anywhere) - some people claim that "exceptions can't just randomly jump out of code" is a benefit of not having exceptions but usually those people sweep OutOfMemory and DivisionByZero under the rug (e.g. Rust, where they just "crash" the program)
Rust would obviously fit the checked exceptions path, as the Result implementatoin basically is "poor-man's checked exceptions". It only needs to flip the syntax sugar - propagate by default (and implicitly), "catch" and materialize using `?` - as well as making Error an open enum (such that you can add cases implicitly - see e.g. OCaml's exception type `exn` [1]) - and that's basically it!
[1] https://ocaml.org/manual/5.3/extensiblevariants.html
I'm well aware of the tendency of Rust programmers to write bad code, constrained by the language, and then be deluded into thinking that that's good code.
This seems to happen a lot in the Rust community when people point out flaws in the language.
You are not a serious person.
You're not a person capable of using logic, apparently. As is characteristic of Rust zealots.
But you skipped over how you are defining, "good code"? Without that part, "doesn't allow" cannot be evaluated in the context of Java or C++ or Python or Go or Rust.
Logic.
> The result type does not work because you have to choose between immediate callers handling failures (they don't always have the context to do so because they're not aware of the context of callers higher up on the call stack) or between propagating all of your error values all the way up the stack to the error handling point and making your program fantastically brittle and insanely hard to refactor.
It's interactions like this that are the reason why my organization isn't adopting Rust.
You've mistaken your personal preferences and aesthetic sense for absolute truth.
The arrogance is astounding.
This is a bad choice to have to make. If you believe otherwise, you are an incompetent software engineer. Full stop. These are not personal preferences or aesthetics - these are facts. If you can't parse why these are bad, then you're incapable of basic logic.
> within our industry
Of course, we can already see that because you're resorting to fallacies instead of addressing the point itself.
> You've failed to recognize there is no objective and universally recognized metric for good code within our industry.
Literally none of that is relevant to the point that I'm making. I don't have to define "good code" in order to point out that something is "bad code".
I'm going to print out this thread and show it to anyone who says that they're considering learning Rust as a warning that this is what the community is like - incapable of using logic, unwilling to admit the slightest fault in their religion despite factual evidence to the contrary, and willing to use dishonest rhetoric and fallacies in their defense.
Counterpoint: there are many non-trivial programs written in Rust, and they use Result for error handling.
You can write any program in COBOL. Most people would say that it's an insufficient language for doing so.
"Insufficient" here obviously does not mean that it's impossible to write non-trivial programs, just that they'll have bad code.
Exactly like try catch
Nothing prevents people from doing their own way (error int codes, bool handling, Result types, etc, panic), but it's just an easiest way that handles well 99% of the error handling cases, so it sticks and gives a nice feeling of predictability of error handling patterns in Go codebases.
In Rust, you can't just skip error handling. You have to proactively do something generally unwise (and highly visible!) like call .unwrap() or you have to actually handle the error condition.
Go still relies on goodwill and a good night's sleep. The Rust compiler will guard against laziness and sleep deprivation, because ultimately programming languages are about people, not the computers.
The conversation function is more language issue. I don’t think there is a simple way of creating a rust equivalent version because C++ has implicit conversions. You could probably create a C++ style turbofish though, parse<uint32_t>([your string]) and have it throw or return std::expected. But you would need to implement that yourself, unless there is some stdlib version I don’t know of.
Don’t conflate language features with library features.
And -Wconversion might be useful for this but I haven’t personally tried it since what Matt is describing with explicit types is the accepted best practice.
I have my gripes with rust, more it’s ecosystem and community that the core language though. I won’t ever say it’s a worse language than C++.
I like the Rust language quite a bit. I find the Rust community to be one of the most toxic places in the entire tech business. Your mileage may vary and that's fine of course - but plenty of people want to stay far away from a community that acts like the Rust community does.
On the surface it sounds like a community with such deep pathology that it will take at least a generation following a complete change of leadership to have a chance at recovery. But there are three sides to every story.
> On the surface it sounds like a community with such deep pathology
First what sort of pathology? You're confusing community with leadership.
The community didn't want this, and leadership was doing a restructuring due to change from Foundation and Project. Welcome to OSS projects.
Second as opposed to what?
A community at the beck and call of your CEO dictator? I'm a Java dev, so all it takes for Java to die is for One Rich Asshole Called Larry Ellison to decide that they (ORACLE) are inserting two mandatory ads to be watched during each Java compiler run. Or god forbid that they will monetize Java.
Plus if I had 24/7 insight into how Oracle worked, I'd probably also be much less inclined to join Java as a new dev.
To paraphrase Tolstoy: (All perfect languages are dead;) Each imperfect language is imperfect in its own way.
I can try to answer your questions if they are important to you, but it will require some significant effort, so I'd like to be sure they aren't rhetorical first.
And how does it differ from the average Open-Source Project?
For example, in your example of "inserting two mandatory ads to be watched during each Java compiler run", the OpenJDK license guarantees your right to remove those ads from your copy of javac, and to redistribute that copy of javac as the official Debian or Ubuntu version of javac. F-Droid does this routinely to such "antifeatures" in open-source Android applications, and of course Android itself exists because the open-source licenses of projects like Harmony, IDEA, and Eclipse protect Google from such malfeasance.
I'm not sure what the "average open-source project" is. Maybe something like https://github.com/justinasr/FantilatorOS/, the firmware for a redundant array of 64 inexpensive muffin fans that someone set up at CERN? https://web.archive.org/web/20231010215437/https://onlyfans.... Or https://github.com/antimatter15/splat, a Gaussian splat renderer in WebGL? I think those are both "non-average" in that they have a very above-average number of forks and stars. But, like the overwhelming majority of open-source projects, neither of them has a community in any meaningful sense, so I think the question doesn't make sense, given that it's a question about community dynamics.
It wasn't a serious example. But if tomorrow Oracle decided to monetize Java (which they have for LTS) there is little you could do.
> I'm not sure what the "average open-source project" is.
Look at Github. Take several random non-fork repos. It's most likely a lib written and maintained by 1-3 people, where last update was 4 years ago.
Maybe that’s more of a bias with rust media stuff, seems to be going deeper into that rabbit hole though.
The community was at least, may still be, very sensitive to rust being criticised. I genuinely brought an example of a provably correct piece of code that the borrow checker wouldn’t accept, interior mutability problem. I was I should build a massive abstraction to avoid the problem and that I’m holding it wrong… Put me off the language for a few years, it shouldn’t have, I should have just ignored the people and continued on but we all get older and learn things.
My favorite is when Rust gets dragged into weird American "culture wars" - somehow, it's a "woke" language? (And somehow, that's a problem?)
But yeah, the language docs are pretty up front about the fact that the borrow checker sometimes rejects code that is provably fine, so it's a weird criticism. The nontrivial breakthrough was that Rust proved that a huge amount of nontrivial code can be written within the restrictions of the borrow checker, eliminating swaths of risk factors without a resource penalty.
Especially if you're coming from different langs.
If I delete/rename a field of a class in any statically checked language, it's going to report a compile error, and it's still a breaking change. Same thing with named arguments.
Typically when you're changing a name you're changing behavior, too. This isn't just something we should let slip under the radar, otherwise bugs can actually manifest.
Even if you don't use keyword args your parameter names are still part of your API surface in Python because callers can directly name positional args. Only recently have you been able enforce unnamed positional only args as well as the opposite.
In fact, I think OCaml has one of the best labeled argument system around there. The only downside is that it doesn't always interact well with currying, but perhaps languages without currying could just copy all the rest.
Just to elaborate a bit, in OCaml you can have functions like:
And you can then call this like foo ~a:1 ~b:42 55 or foo ~a:2 ?b:None 55. But then forwarding those optional parameters works like: and the optional parameter b will be forwarded as an optional parameter.Given Rust's historical relations with OCaml I'm slightly disappointed that it doesn't have the same labeled and optional argument system.
Quantity(100) is counterproductive here, as that doesn't narrow the type, it does the opposite, it casts whatever value is given to the type, so even Quantity(100.5) will still work, while just plain 100.5 would have given an error with '-Wconversion'.
Additionally, `clang-tidy` catches this via `bugprone-narrowing-conversions` and your linter will alert if properly configured.
I do run clippy on my Rust projects, but that's a matter of style and readability, not correctness (for the most part!).
Conversions may be fine and even useful in many cases, in this case it isn’t. Converting to std::variant or std::optional are some of those cases that are really nice.
I appreciate that there are guardrails in a tool like rust, I also appreciate that sharp tools like c exist, they both have advantages.
There are also more type-safe conversion methods that perform a more focused conversion. Eg a widening conversion from i8 -> i16 can be done with .into(), a narrowing conversion from i16 -> i8 can be done with .try_into() (which returns a Result and forces you to handle the overflow case), a signed to unsigned reinterpretation like i64 -> u64 can be done with .cast_unsigned(), and so on. Unlike `as` these have the advantage that they stop compiling if the original value changes type; eg if you refactor something and the i8 in the first example becomes an i32, the i32 -> i16 conversion is no longer a widening conversion so the `.into()` will fail to compile.
And don't get me started on dynamic graphs.
I would happily use Rust over C++ if it had all other improvements but similar memory management. I am completely unproductive with Rust model.
(filled with boilerplate, strange Rust idioms, borrow_unchecked, phantomdata, and you still have to manage lifetimes annotations).
There's exactly as much as there was before though. The entire point of the Rust safety paradigm is that you can guarantee that unsafe code is confined to only where it is needed. Nobody ever promised "you will never have to write unsafe code", because that would be clearly unfeasible for the systems programming domain Rust is trying to work in.
I frankly cannot understand why people are so willing to throw the baby out with the bathwater when it comes to Rust safety. It makes no sense to me to say "my code needs to have some % unsafe, so I'll just make it 100% unsafe then" (which is effectively what one does when they use C or C++ instead). Why insist on not taking any safety gains at all when one can't have 100% gain?
All safe code is built on a foundation of unsafe code.
[1] https://doc.rust-lang.org/std/cmp/struct.Reverse.html
But once you are _maintaining_ applications, man it really does feel like absolute magic. It's amazing how worry-free it feels in many respects.
Plus, once you do embrace it, become familiar, and start forward-thinking about these things, especially in areas that aren't every-nanosecond-counts performance-wise and can simply `Arc<>` and `.clone()` where you need to, it is really quite lovely and you do dramatically less fighting.
Rust is still missing a lot of features that other more-modern languages have, no doubt, but it's been a great ride in my experience.
The idea with Rust is that you get safety...not that you get safety at the cost of performance. The language forces you into paying a performance cost for using patterns when it is relatively easy for a human to reason about safety (imo).
You can use `unsafe` but you naturally ask yourself why I am using Rust (not rational, but true). You can use lifetimes but, personally, every time I have tried to use them I haven't been able to indicate to the compiler that my code is actually safe.
In particular, the protections for double-free and free before use are extremely limiting, and it is possible to reason about these particular bugs in other ways (i.e. defer in Go and Zig) in a way that doesn't force you to change the way you code.
Rust is good in many ways but the specific problem mentioned at the top of this chain is a big issue. Just saying: don't use this type of data structure unless you pay performance cost isn't an actual solution to the problem. The problem with Rust is that it tries to force safety but doesn't have good ways for devs to tell the compiler code is safe...that is a fundamental weakness.
I use Rust quite a bit, it isn't a terrible language and is worth learning but these are big issues. I would have reservations using the language in my own company, rather than someone else's, and if I need to manage memory then I would look elsewhere atm. Due to the size of the community, it is very hard not to use Rust too (for example, Zig is great...but no-one uses it).
The pragmatism of Rust means that you can use reference counting if it suits your use case.
Unsafe also doesn't mean throwing out the Rustiness of Rust, but others have written more extensively about that and I have no personal experience with it.
> The problem with Rust is that it tries to force safety but doesn't have good ways for devs to tell the compiler code is safe...that is a fundamental weakness.
My understanding is that this is the purpose of unsafe, but again, I can't argue against these points from a standpoint of experience, having stuck pretty strictly to safe Rust.
Definitely agree that there are issues with the language, no argument there! So do the maintainers!
> if I need to manage memory then I would look elsewhere atm
Haha I have the exact opposite feeling! I wouldn't try to manage memory any other way, and I'm guessing it's because memory management is more intuitive and well understood by you than by me. I'm lazy and very much like having the compiler do the bulk of the thinking for me. I'm also happy that Rust allows for folks like me to pay a little performance cost and do things a little bit easier while maintaining correctness. For the turbo-coders out there that want the speed and the correctness, Rust has the capability, but depending on your use case (like linked lists) it can definitely be more difficult to express correctness to the compiler.
I think the issue that people have is that they come into Rust with the expectation that these problems are actually solved. As I said, it would be nice if lifetimes weren't so impossible to use.
The compiler isn't doing the thinking if you have to change your code so the compiler is happy. The problem with Rust is too much thinking: you try something, compiler complains, what is the issue here, can i try this, still complain, what about this, etc. There are specific categories of bugs that Rust is trying to fix that don't require the changes that Rust requires in order to ensure correctness...if you use reference counter, you can have more bugs.
You gotta get your timing right. Right hook followed by kidney shot works every time.
And while unsafe Rust does have some gotchas that vanilla modern C++ does not, I would much rather have a 99% memory-safe code base in Rust than a 100% "who knows" code base in C++.
Even Rust's types aren't going to help you if two arguments simply have the same types.
Or another (dummy) example transfer(accountA, accountB). Make two types that wrap the same type but one being a TargetAccount and the other SourceAccount.
Use the type system to help you, don’t fight it.
Sound type systems are equivalent to proof systems.
You can use them to design data structures where their mere eventual existence guarantee the coherence and validity of your program’s state.
The basic example is “Fin n” that carries at compile time the proof that you made the necessary bounds checks at runtime or by construction that you never exceeded some bound.
Some languages allow you to build entire type level state machines! (eg. to represent these transactions and transitions)
My median Rust coding session isn't much different, I also write code that doesn't work, but it's caught by the compiler. Now, most people call this "fighting with the borrow checker" but I call it "avoiding segfaults before they happen" because when I finally get through the compiler my code usually "just works". It's that magical property Haskell has, Rust also has it to a large extent.
So then what's different about Rust vs. C++? Well Rust actually provides me a path to get to a working program whereas C++ just leaves me with an error message and a treasure map.
What this means is that although I'm a bad programmer, Rust gives me the support I need to build quite large programs on my own. And that extends to the crate ecosystem as well, where they make it very easy to build and link third party libraries, whereas with C++ ld just tells you that it had a problem and you're left on your own to figure out exactly what.
There are some places I won’t be excited to use rust, and media heavy code is one of those places…
Give an example. I have been programming in C/C++ for close to 30 years and the places where I worked had very strict guidelines on C++ usage. We could count the number of times we shot ourselves due to the language.
Low level languages always came with a set of usage guidelines. Either you make the language safe for anyone that they can't shoot themselves in the foot and end up sacrificing performance, or provide guidelines on how to use it while retaining the ability to extract maximum performance from the hardware.
C/C++ shouldn't be approached like programming in Javascript/Python/Perl.
I suppose it could still be added in the future; there are probably several syntax options that would be fully backward-compatible, without even needing a new Rust edition.
Sorry, I don't agree.
First, code is read far more often than written. The few seconds it takes to type out the arguments are paid again and again each time you have to read it.
Second, this is one of the few things that autocomplete is really good at.
Third, almost everybody configures their IDE to display the names anyway. So, you might as well put them into the source code so people reading the code without an IDE gain the benefit, too.
Finally, yes, they are redundant. That's the point. If the upstream changes something and renames the argument without changing the type I probably want to review it anyway.
So is this really a language comparison, or what libraries are available for each language platform? If the latter, that's fine. But let's be clear about what the issue is. It's not the language, it's what libraries are included out of the box.
https://en.m.wikipedia.org/wiki/Linguistic_relativity
From your link:
> Nevertheless, research has produced positive empirical evidence supporting a weaker version of linguistic relativity:[5][4] that a language's structures influence a speaker's perceptions, without strictly limiting or obstructing them.
To entertain the argument, though, it may not be a language issue, but it certainly is a selling point for the language (which to me indicates a "language issue") to me if the language takes care of this "library" (or good defaults as I might call them) for you with no additional effort -- including tight compiler and tooling integration. That's not to say Rust always has good defaults, but I think the author's point is that if you compare them apples-to-oranges, it does highlight the different focuses and feature sets.
I'm not a C++ expert by any stretch, so it's certainly a possibility that such a library exists that makes Rust's type system obsolete in this discussion around correctness, but I'm not aware of it. And I would be incredibly surprised if it held its ground in comparison to Rust in every respect!
And while we're at it, why not use assembly? It's all just "syntactic sugar" over bits, doesn't make any difference, right?
https://play.rust-lang.org/?version=stable&mode=debug&editio... https://play.rust-lang.org/?version=stable&mode=debug&editio...
By default, they're on during debug mode and off in release mode.
There are also specific methods for doing *erflow-checked arithmetic if you like.
[0]: https://www.felixcloutier.com/x86/add
Rust developers made a poor choice. They should have made a special function for unchecked addition and have "+" operator always panic on overflow.
The checks being on in the debug config means your tests and replications of bug reports will catch overflow if they occur. If you are working on some sensitive application where you can't afford logic bugs from overflows but can afford panics/crashes, you can just turn on checks in release mode.
If you are working on a library which is meant to do something sensible on overflow, you can use the wide variety of member functions such as 'wrapping_add' or 'checked_add' to control what happens on overflow regardless of build configuration.
Finally, if your application can't afford to have logic bugs from overflows and also can't panic, you can use kani [0] to prove that overflow never happens.
All in all, it seems to me like Rust supports a wide variety of use cases pretty nicely.
[0]: https://github.com/model-checking/kani
Just insist that the programmer prove that overflow can't occur, and reject programs where the programmer couldn't or wouldn't do this.
This line could only overflow after we need to grow the container, so immediately this means the type T isn't a ZST as the Vec for ZSTs doesn't need storage and so it never grows.
Because its not a ZST the maximum capacity in Rust is never bigger than isize::MAX which is an entire binary order of magnitude smaller than usize::MAX, as a result len + 1 can't overflow the unsigned type, so this code is correct as written.
https://learn.adacore.com/courses/intro-to-ada/chapters/stro...
And there is an Ada implementation that is part of GCC:
https://www.gnu.org/software/gnat/
For comparison, Swift uses "+" for checked addition and as a result, majority of developers use checked addition by default. And in Rust due to its poor design choices most developers use wrapping addition even where a checked addition should be used.
[1] https://doc.rust-lang.org/src/alloc/vec/mod.rs.html#2010
Your example code is not because it is faster to write, it is because it is impossible for its to overflow on that line.
Or just because on Intel CPUs it has overhead, we must forget about writing safer code?
Of course, if you don't trust the standard library, you can turn on overflow checks in release mode too. However, the standard library is well tested and I think most people would appreciate the speed from eliding redundant checks.
By the way, using "atoi" in a code snippet in 2025 and complaining that it is "not ideal" is, well, not ideal.
Meaning you're in a context where you have control on the C++ code you get to write. In my company, lots of people get to update code without strict guidelines. As a result, the code is going to be complex. I'd rather have a simpler and more restrictive language and I'll always favor Rust projects to C++ ones.
Of course it will probably not be as bad as C++, but still it will be complex and people will be looking for a simpler language.
There will always remain two types of languages: those that nobody uses and those that everybody complains about.
That's not a good reason to stick with inferior tools now, though.
Rust is inferior to C++ for my needs. This is just a reflection on we started a large project in C++ before rust existed, and now have millions of lines. Getting Rust to work with our existing C++ is hard enough as to not be worth it. Rewriting in Rust would cost 1 billion dollars. Thus despite all the problems we have with C++ that Rust would solve, rust is inferior.
(Rust is working on their C++ interoperability story and we are making changes that will allow using Rust in the future so I reserve the right to change this story in a few years, but only time will tell)
There's a lot of warts here, particularly around the fact that all Rust types are "trivially relocatable" in C++ parlance. At the same time, figuring out which C++ types are trivially relocatable is pretty difficult. To give you an idea of the current situation, all or most non-POD C++ types must be "pinned" on the Rust side, forcing you to deal with the rather clunky `Pin<&mut T>` API. Either that or heap-allocating all C++ types owned from Rust code. Not great.
The story is likely to improve, hopefully. There's an interesting overview here: https://hackmd.io/@rust-lang-team/rJvv36hq1e
Modern C++ has reduced a lot of typing through type inference, but otherwise the language is still strongly typed and essentially the same.
Meanwhile it was one of the reasons after Turbo Pascal, my next favourite programming language became C++.
For me mastering C, after 1992, only became mattered because as professional, that is something that occasionally I have to delve into so better know your tools even if the grip itself has sharp corners, otherwise everytime the option was constrained to either C or C++, I always pick C++.
It _is_ statically typed, though, so it falls in a weird category of loosely _and_ statically typed languages.
That aside, the only remaining footgun in C++ is the implicit numeric conversions. What else did you have in mind?
Then you have all the shenanigans around placement-new and vtables.
If it isn't downright weak, it's also not particularly strong.
OTOH placement-new is pretty much impossible to use by accident. If used intentionally, I don't see it as being any different from an explicit cast - again, you get what you signed up for.
Interestingly in some ways C++ is arguably more typesafe than languages like Java or C#, given how it handles dynamic type of object during construction & destruction...
You CAN write nice modern code in C++, but the ability to force yourself and all your colleagues to do so in perpetuity isn't really there yet.
Although it might be in the future, which would be nice.
I tried again recently for a proxy I was writing thinking surely things have evolved at this point. Every single package manager couldn’t handle my very basic and very popular dependencies. I mean I tried every single one. This is completely insane to me.
Not to mention just figuring out how to build it after that which was a massive headache and an ongoing one.
Compared to Rust it’s just night and day.
Outside of embedded programming or some special use cases I have literally no idea why anyone would ever write C++. I’m convinced it’s a bunch of masochists
Well there's your problem - no serious project uses one.
> I’m convinced it’s a bunch of masochists
People use cpp because it's a mature language with mature tooling and an enormous number of mature libraries. Same exact reason anyone uses any language for serious work.
Just today, I spent 2 hours trying to get an OS kernel (SeL4) to compile for ARM64. This is an officially supported platform. I ran into a series of problems:
- A recent update to sel4 meant I need to set -DCONFIG_ARM_TLS_REG_TPIDRU for thread-local storage. (Leaving it out is a compiler error). This flag isn't in the configuration script - and it took me awhile to figure out how to set it through cmake + ninja.
- The gcc-aarch64-linux-gnu toolchain was recommended somewhere in the docs. On newer versions of gcc, that pulls in libgcc_eh.a, which tries to use _dl_find_object - which is of course linux only. So the build fails.
- So I swapped to gcc14's aarch64-none-elf cross compiler toolchain, which doesn't assume a dynamic linker is available. gcc-aarch64-none-elf isn't available in apt for some reason, even though the 32 bit counterpart is. From there, I ran into a new compiler error. Turns out the arm gcc toolchains are built against an old version of binutils that didn't have support for SHF_GNU_RETAIN. That is needed for the retain macro to work in C. And retain is used by sel4. This is just a compiler warning, but because sel4 promotes all warnings to errors, the build fails. I could have built my own compiler with a newer binutils. But I ended up just disabling the warning, because you only live once. I briefly tried gcc11 because that's recommended elsewhere in the docs, but gcc11 has the same problem.
All of this headache was from trying to build sel4 - which is all straight C, and one of the highest quality C projects I've ever seen. The codebase is mathematically proven to be correct. But oh my goodness, what a horrendous mess it is trying to work with it. And of course, just to try and get the build process going, I needed to install multiple compilers, ninja, cmake, python and a bunch of random python packages, and google's 'repo' tool to manage dependencies. What is this junk.
Personally I'd prefer to work with clang, but I've had enough pain for one day. And I know swapping compilers will for sure break something else.
If sel4 was written in zig or rust, it would be 10x easier to build and work with. Zig handles cross compilation to any supported platform out of the box. Rust can install toolchains trivially with rustup. Rust also has consistent stability guarantees, ensuring code doesn't "rot" with new compiler versions. And of course, both handle dependencies much, much better. And no need for cmake / ninja / random python scripts to compile. Zig and rust also don't behave differently based on someone else's binutils version. Wtf.
If C is too "mature" to fix these problems, I'm going to switch languages. Sel4 is only 20k lines of code. I wonder if it'd be less work to port it to zig than work with it in C. (I'd consider rust, but rust code can't prevent panics.)
C++ "gets away" with it because of templates. Many (most?) libraries are mostly templates, or at the very least contain templates. So you're forced into include-style dependencies and it's pretty painless. For a good library, it's often downloading a single file and just #include-ing it.
C++ is getting modules now, and maybe that will spur a new interest in package managers. Or maybe not, it might be too late.
The shenanigans people get into with CMake, Conan, vcpkg, and so on is a patchwork of nightmares and a huge time sink compared to superior solutions that people have gotten used to in other languages, including Rust.
I agree overall that the C or C++ way of doing things is more cumbersome, but I don't think that's enough to write off those languages as a whole.
C++ build systems are notoriously brittle. When porting a project to a new platform, you're never just porting the code, you are also porting your build system. Every single project is bespoke in some way, sometimes because of taste, but most of the time because of necessity.
It works because people spend a huge amount of time to make it work.
It’s the same build system for all of them.
Everyone know the system is brittle, but somehow manage to handle it.
FAANG, hedge funds/HFT, game studios
I am no expert so take it with a grain of salt, but that was how it felt for me.
With Nix, the package selection is great and repackaging is fairly straight forward.
Citation needed. The empiric evidence I’ve seen has shown the opposite.
https://rustfoundation.org/media/unsafe-rust-in-the-wild-not...
https://thenewstack.io/unsafe-rust-in-the-wild/
https://github.com/sslab-gatech/Rudra/blob/master/rudra-sosp...
https://internals.rust-lang.org/t/canvas-unsafe-code-in-the-...
Again, not a criticism of the language itself per se, and certainly not trying to imply that all Rust programmers go around writing unsafe code all day long. It's just kind of funny to me that it is touted as being so much more safe than C++ when the reality is that at least some of those features are in fact disabled for one reason or another in real-world programs.
This is very different than
> in the name of speed, efficiency, etc
And also does not demonstrate that
> the end result being a program which isn't much more secure than its C++ equivalent.
At all.
Lets say I am doing something with V8, CLR, JVM, GCC, LLVM, CUDA, Metal, outside the managed languages that sit on top of those runtimes, compiler toolchains, GPGPU.
That infrastructure code is written in C++, and making use of Rust means adding yet another layer on how to compile, interop and debug, on top of what is already there.
As for safety in general, I am a believer of systems programming languages with automatic memory management, and polyglot programming, we aren't yet there because industry has the tendency to take decades to finally adopt good ideas, and only on a partial way.
Thus while I appreciate what Rust has achieved bringing affine types into mainstream, not an easy job, I consider this a transition step until we get both approaches in the same language, automatic resource management with affine,linear,effects,dependent types.
However this ultimately won't matter as much, when we eventually get our AI buddies good enough that they can spew executables directly.