It is a bit unclear to me why somebody who rejects C++ because "I once spent an entire year in the heaven of C++, walking around in a glorious daze of std::vector and RAII, before one day snapping out of it and realizing that I was just spawning complexity that is unrelated to the problem at hand." (which I can absolutely agree with!) is picking Rust from all options. If there is a language that can rival C++ in terms of complexity, it is Rust.
I've seen this idea from a few people and I don't get it at all.
Rust is certainly not the simplest language you'll run into, but C++ is incredibly baroque, they're not really comparable on this axis.
One difference which is already important and I think will grow only more important over time is that Rust's Editions give it permission to go back and fix things, so it does - where in C++ it's like venturing into a hoarder's home when you trip over things which are abandoned in favour of a newer shinier alternative.
C++'s complexity is coming from how eager it is to let you shoot yourself in the foot. Rust will make you sweat blood to prove the bird in the sky you're shooting at is really not your own foot.
Yes, I think it's pretty easy to be sure that this isn't where the complexity comes from when there's literally a method to do that on arrays. I'm not even sure which language you're talking about here, because you can call `.len()` in Rust or `.size()` in C++.
If you're trying to remember what the language is where there's no immediately obvious straightforward way to get the length of an array, it's not Rust or C++; you must have been thinking of C.
This is a case of exactly the "hoarder" modality in C++ that I described. When somebody says array in C++ you think of std::array, the newer shiny C++ class which you're asked to use instead of the native array types. The native array types in C++ still exist and indeed do not provide methods like size()
In Rust the arrays aren't stunted left over primitive types which weren't gifted modern features, array.len() works because all Rust's types get features like this, not just the latest and greatest stuff.
While it is true that only class types have member functions in C++, that does not mean that objects of other types demand the use of macros. C++ also supports non-member functions, and the standard library contains a fair amount of these; including `std::size` that can be used to "get" the length of an array.
(C++ arrays are different from arrays in many other programming languages, though not necessarily Rust, in that their type specifies their length, so in a way this is something you already "have" but certainly there are cases where it is convenient to "get" this information from an object.)
Because of the type system, with its ML influence, two macro systems, the stuff on nightly that many folks enjoy using, having to rely on external crates for proper error handling and async/await features.
Additionally, given its ML influence, too many people enjoy doing Haskell level FP programming in Rust, which puts off those not yet skilled in the FP arts.
Also the borrow checker is the Rust version of Haskell burrito blogs with monads, it is hard to get how to design with it in mind, and when one gets it, it isn't that easy to explain to others still trying to figure it out.
Hence why from the outside people get this opinion over Rust.
Naturally those of us with experience in compilers, type systems theory and such, see it differently, we are at another level of understanding.
> Also the borrow checker is the Rust version of Haskell burrito blogs with monads, it is hard to get how to design with it in mind,
Eh. Haskell monads are a math-centric way of performing operations on wrapped types (stuff inside a monad). Rust borrow checker is way more pragmatic-centric, i.e. how can we prevent certain behaviors.
The difference being you don't see Monads being replaced by Tree-Monads, without impacting the code.
> and when one gets it, it isn't that easy to explain to others still trying to figure it out.
So is going from 0-based to X-based arrays (where X is an integer); Or learning a new keyboard layout. Just because it's hard (unfamiliar) doesn't mean it's impossible.
>where in C++ it's like venturing into a hoarder's home when you trip over things which are abandoned in favour of a newer shinier alternative
Perl is so bad about this that I once worked on a very old codebase in which I could tell approximately when it was written based on which features were being used.
Because they’re both complicated languages, but for different reasons. Rust didn’t really solve memory safety, it just pushed all the complexity into the type system. If one was struggling with memory errors in C++ that’s nice. If one was using Java, that still sucks.
Furthermore some programmers really like complicated languages like Rust, Haskell, etc others like straightforward languages like Go, Python, etc.
Python is absolutely not straighforward, it's a huge language with many moving parts and gotchas.
Although, I admit, both are very easy to start programming in and to start shipping broken projects that appear working at first. They are good for learning programming, but terrible for production.
> Rust didn’t really solve memory safety, it just pushed all the complexity into the type system.
Yes, that's what it did, and that's the right tradeoff in most cases. Where the compiler can't make a reasonable default choice, it should force the programmer to make a choice, and then sanity-check it.
> If one was struggling with memory errors in C++ that’s nice. If one was using Java, that still sucks.
It's nice for those struggling with uncaught exceptions, null pointer bugs, mutithreading bugs, and confusing stateful object graphs in Java.
Many of us strongly prefer handling some complexity ourselves that we can then tackle with tests, CI, fuzzing, etc if it means we don’t have to jump through hoops to satisfy the compiler before we can even see our code running.
Yes, Golang and Python and Java are very easy to start programming in. And unless we’re dealing with some really complex problem, like next-gen cryptocurrencies ;), by the time the Rust teams have gotten their code working and nice and proper as any Rust code needs to be, the Golang/Python/Java teams have already released to customers.
If one wants to be super-cautios and move accordingly slower to be really really sure they have no memory errors then that’s fine. There’s a market for that stuff. But selling this approach as a general solution is disingenuous.
> the Golang/Python/Java teams have already released to customers.
...have already released a broken prototype that appears to be working for now.
I'm yet to see a case where manually "hardening" your software is faster than writing a "similarly-good" program in Rust. That's just anti-automation. Why repeat the same work in every project and bloat your codebase, when the compiler can carry that burden for you? In my experience, Rust makes you write production-grade software faster than when using other languages.
> But selling this approach as a general solution is disingenuous.
I agree! There are legitimate cases where releasing a broken prototype as quickly as possible is important. There's room for that.
But I agrue that it's not the case for most "serious" production software that would be maintained for any period of time. And that Rust is the preferable option for writing such production software.
The idea that one needs Rust to write reliable software is not only ridiculous at a logical level, it is also contradicted by the fact that Rust is but a tiny, irrelevant subset of safety-critical software or really all software.
If it were really “preferable for writing such production software”, more people would be using it.
But they don’t because the compiler does not carry the burden for you. It puts the burden of writing code in a way that satisfies the Rust lifetime management design on you, and that’s what you’ll be doing forever.
There zero proof that Rust software is higher quality than equivalent e.g. Swift or Java. The memory-safety trick only works against C and C++.
> The idea that one needs Rust to write reliable software is not only ridiculous at a logical level
I never said that. I said that it's more appropriate and productive.
> If it were really “preferable for writing such production software”, more people would be using it.
There are many valid reasons for not using it, even in new projects. Such as human preferences, lack of specific libraries, and simply not having time to learn yet another language for a non-10x benefit.
> the compiler does not carry the burden for you. It puts the burden of writing code in a way that satisfies the Rust lifetime management design on you, and that’s what you’ll be doing forever.
It carries a different burden for me. In exchange for the burden that you mention, I don't worry about mutable aliasing bugs (including data races), breaking the code by refactoring it, manually unit-testing basic properties that could be types, debugging non-obvious non-local effects (such as exceptions and null values invisibly propagating through the layers of the app), micro-optimizing the performance manually, and so on. The benefits are known. I've almost forgot how to use a debugger at this point.
It's a tradeoff. I find it more comfortable and less cognitively-taxing to work this way, once the initial learning curve is behind. Less unpredictable problems to distract you from solving the problem. The borrow checker problems largely go away as you get more comfortable with it. And it's a skill that's instantly transferrable between all Rust projects, unlike project-specific defensive practices in other languages (the "manual hardening" needed to get decent quality). It's a long-term investment that automates a small, but not insignificant part of your work.
> There zero proof that Rust software is higher quality than equivalent e.g. Swift or Java.
Yeah, the thing is, the moment your software has crossed the boundary of success, it becomes a burden, not a blessing. You are now stuck with it and you better have made sure that you're happy with it the way you built it, because you're not moving on from this project any time soon.
Maybe Rust isn't optimized for throwaway projects and that's fine.
I think you can do throwaway projects and prototypes about as fast in rust.
But it requires a different mindset which I think many in our industry finds hard to work with. Rust exposes the imperfections rather than hiding them until you explicitly check for them like in most other languages.
Just clone and unwrap liberally and throwaway projects go fast.
What we’re seeing in practice is not throwaway software but successful companies built over many years using e.g. Golang. When they grow big enough to pay the Rust tax and can’t squeeze more performance out of their set-up they switch to Rust.
Starting something in Rust only makes sense for very few domains and software categories.
I wonder how Go would fare if it had a production-ready LLVM/GCC backend. I wouldn't be surprised if much of the performance differences between Go and C/C++/Rust comes down to optimized codegen (rather than GC, which is what people often complain about Go). Not saying that GC pauses might not be an issue in some cases, but still...
As someone who has to work with a janky mix of C++ and Python. I would prefer getting rid of both altogether. I personally am not a fan of Python or the Python ecosystem whatsoever. It's certainly not speeding things up with those two.
Go pushes the complexity into the programmer, that is how one ends with source code like Kubernetes, when the language doesn't provide all the tooling.
Python belongs to the complicated languages section, people that think Python is straightforward never bothered reading all the manuals, nor had to put up with all the breaking changes throughout its history, it wasn't only 2 => 3, that was the major event, every release breaks something, even if little.
> Go pushes the complexity into the programmer, that is how one ends with source code like Kubernetes, when the language doesn't provide all the tooling.
I am not sure what you mean by this. I write go code pretty much everyday and that code looks vaguely the same as it would do in C#, or Python, or JavaScript.
> Python belongs to the complicated languages section, people that think Python is straightforward never bothered reading all the manuals, nor had to put up with all the breaking changes throughout its history, it wasn't only 2 => 3, that was the major event, every release breaks something, even if little.
I've read the manuals. Most docs appear to be reasonably well written, even if I find many of the examples a bit odd (I've never been a big fan of Monty Python, so the spam and eggs examples are all a bit odd).
The 2 => 3 change was probably a big thing for those migrating all the existing libraries, frameworks. But as someone that uses it for prototyping stuff, I barely noticed the difference.
C++ has editions too btw. C++11, C++14, C++17, etc. These are opt in and allowed to break compatibility, although that is very rarely done in practice.
That's one difference. And the other important differences are:
- Rust apps can depend on library "headers" written in other editions. That's the whole deal with editions! Breaking changes are local to your own code and don't fracture the ecosystem.
- Rust has a built-in tool that automatically migrates your code to the next edition while preserving its behavior. In C++, upgrading to the next standard is left as an exercise for the reader (just like everything else). And that's why it's done so rarely and so slowly.
Not really, because the crates they depend on cannot expose APIs with semantic changes across versions.
Also it requires everything to be compiled with the same compiler, from source code.
There are tools available in some C++ compilers for migration like clang, note the difference between ISO languages with multiple implementations, and one driven by its reference compiler.
> Not really, because the crates they depend on cannot expose APIs with semantic changes across versions.
Not sure what you're talking about. Any specific examples?
> Also it requires everything to be compiled with the same compiler, from source code.
It's not related to editions at all. It's related to not having an implicit stable ABI.
It's possible have a dynamic Rust library that exposes a repr(C) interface, compile it into an .so using one version of the compiler, and then compile the dependent "pure Rust" crates using another compiler that's just going to read the metadata ("headers") of that library and link the final binary together. Same as in C and C++. You just can't compile any Rust code into a stable dynamic library, by defalut. (You can still always compile into a dylib that needs a specific compiler version)
As the other commenter responded there, your example isn't about editions at all. It's about mixing ABIs and mixing multiple versions of the language runtime. Those are entirely separate issues.
You're correct that the possible changes in editions are very limited. But editions don't hinder interoperability in any way. They are designed not to. Today, there are no interoperability problems caused by editions specifically.
> compromises will be required, specially regarding possible incompatible semantic differences across editions.
That's just an assumption in your head. 4 editions later, it still hasn't manifested in any way.
4 editions later, Rust is yet to be used at the scale of C and C++ across the industry, my point on the comment is how editions will look like after 50 years of Rust history.
ABIs and multiple versions of the language runtime, are part of what defines a language ecosystem, hence why editions don't really cover as much as people think they do.
Editions will look like band-aids that don't fully solve the cruft accumulated over the 50 years. That's not hard to predict. It's still a very useful mechanism that slows down the accumulation of said cruft. I'm yet to see a language that has a better stability/evolution story
In any case, while I as language geek have these kind of discussions, given current progress in AI systems, my point of view is that we will get the next evolution in programming systems, thus it won't matter much if it is C, C++, Rust, C#, Java, Go or whatever.
We (as in the industry) will eventually get reliable ways to generate applications directly to machine code, just as optimizing compilers took a couple of decades to beat hand written Assembly, and generate reliable optimized code.
So editions, regardless of what they offer, might not be as relevant in such a timeframe from a couple of decades ahead.
The machine code will always be generated from "something". A one-line informal prompt isn't enough. There will always be people who write specs. Even the current languages are already far from "machine code" and could be considered "specs", albeit low-level
As the response to that comment points out, you are confusing editions for ABI changes. Different editions are purely a source-level change in the language. All editions are ABI-compatible.
Not really, because one thing that apparently I haven't gotten across is that they don't cover semantic changes, only grammar ones for the most part.
What is the compiler supposed to generate if code from edition X calls code in edition X + 10, with a lambda written in X + 5, expecting using a specific language construct that has changed semantics across editions, maybe even more than once?
> one thing that apparently I haven't gotten across is that they don't cover semantic changes, only grammar ones for the most part.
You have gotten it across just fine.
We're trying to get across that no one is ever going to do "global" incompatible semantic changes in editions. That's been understood from the start. Exactly because of the problems that you describe.
It would work as intended? I’m not sure what problem you are trying to point out. The lambda would compile in the edition in which it is written, resulting in a code unit passed to the other crate(s). As mentioned, the ABI is stable across editions. To put in your words, there are no semantic changes to the ABI across editions.
C++ shipping new and slightly incompatible versions of the entire language every three years isn't Editions, there was a proposal to attempt Editions (under the name "Epochs") for C++ but it faced significant headwinds and was abandoned.
I am a fan of Rust but it’s definitely a terse language.
However there are definitely signs that they have thought about making it as readable as possible (by omitting implicit things unless they’re overwritten, like lifetimes).
I’m reminded also about a passage in a programming book I once read about “the right level of abstraction”. The best level of abstraction is the one that cuts to the meat of your problem the quickest - spending a significant amount of time rebuilding the same abstractions over and over (which, is unfortunately often the case in C/C++) is not actually more simple, even if the language specifications themselves are simpler.
C codebases in particular, to me, are nearly inscrutable unless I spend a good amount of time unpicking the layers of abstractions that people need to write to make something functional.
I still agree that Rust is a complex language, but I think that largely just means it’s frontloading. a lot of the understanding about certain abstractions.
I do not really agree with respect to C. I often have to deal with C code written by unexperienced programmers. It is always relatively easy to refactor it step by step. For C++ this is much more painful because all the tools invented to make the code look concise make it extremely hard to follow and change. From the feature set, I would say that Rust is the same (but I have no experience refactoring Rust code).
But you're willing to write many comments complaining that Rust is hard to refactor. Rust is the easiest language to refactor with I've ever worked in, and I've used a couple dozen or so. When you want to change something, you change it, and then fix compiler errors until it stops complaining. Then you run it, and it works the first time you run it. It's an incredible experience to worry so little about unknown side-effects.
> But you're willing to write many comments complaining that Rust is hard to refactor.
Their refactoring comments look focused on C versus C++ to me, with a bit of guessing Rust is like C++ in a way that is clearly labeled as speculation.
So I don't see the problem with anything they said about refactoring.
Also, although it was not designed with this in mind, the described process adopts VERY well to LLMs. You can make a refactoring change, then tell the LLM to "run cargo check and fix the errors." And it does a very good job of doing this.
An LLM will do it with fewer clicks, but the rust-analyzer LSP hook on your editor also helps tremendously.
Both the LLM and humans doing it are relying on the fabulous work the rust compiler team has done in generating error messages with clear instructions on how to fix problems.
> Rust is the easiest language to refactor with I've ever worked in
Oh? So how do you refactor a closure into a named function in Rust?
I have found this to be of the most common failure modes that makes people want to punch the monitor.
(Context: in almost all programming languages, a closure is practically equivalent to an unnamed function--refactoring a closure to a named function tends to be pretty straightforward. This isn't true for Rust--closures pick up variable lifetime information that can be excruciatingly difficult to unwind to a named function.)
Set the type of all the parameters to your new function to bool, then compile. The compiler error will tell you, "Error: you passed a foo<'bar, baz> instead of a bool". Then you change the type in the function's parameter to "foo<'bar, baz>".
In personal experience, the much stronger and composite type model in Rust makes it easier to refactor.
Adding features in particular is a breeze and automatically the compiler/language will track for you the places that use only old set of traits.
Tooling is still newer though and needs polish. Generic handling is interesting at times and there are related missing features for that in the language, vis a vis specializations in particular.
Basic concurrency handling is also quite different in Rust than other languages, but thus usually safer.
I am wondering about this. In C the nice thing is that you usually change things locally. A line of code does not depend on a million other things. I can't quite see how this works in Rust... At least in C++, template, overloading, etc. can make a single line depend on a lot of things.
You won't understand it unless refactor some Rust programs.
Bunny summed it up rather well. He said in most languages, when pull on some thread, you end disappearing into a knot and you're changes are just creating a bigger knot. In Rust, when pull on a thread, the language tells you where it leads. Creating a bigger knot generally leads to compile errors.
Actually he didn't say that, but I can't find the quote. I hope it was something like that. Nonetheless he was 100% spot on. That complexity you bemoan about the language is certainly there - but it's different to what you have experienced before.
In most languages, complex features tend lead to complex code. That's what made me give up on Python in the end. When you start learning Python, it seems a delightfully simple yet powerful language. But then you discover metaclasses, monkey patching, and decorators, which all seem like powerful and useful tools, and you use them to do cool things. I twisted Python's syntax into grammar productions, so you could write normal looking python code that got turned into an LR(1) parser for example. Then you discover other peoples code that uses those features to produce some other cute syntax, and it has a bug, and when you look closely your brain explodes.
As you say C doesn't have that problem, because it's such a simple language. C++ does have that problem, because it's a very complex language. I'm guessing you are making deduction from those two examples that complex languages lead to hard to understand code. But Rust is the counter example. Rust's complexity is forces you to write simple code. Turns out it's the complexity of the code that matters, not the complexity of the language.
I only have limited experience with Rust (only playing around a bit). But it seems the language forces you to structure the code in a specific way (and you seem to agree). This is good, because it prevents you from making a mess (at some level at least). But what others report (and it matches my limited experience) is that it makes the structure of the code very rigid. So I do not quite see how this does not limit refactoring? What some of you describe in this thread is type-directed refactoring (i.e. you change a type and the compiler tells what you need to change), but is this not limited to relatively basic changes?
In C, you can make partial changes and accept a temporary inconsistency. This gives you a lot of flexibility that I find helpful.
I'll put it like this. Rust causes you to refactor more often and do a full refactoring at once (because the interfaces are so rigid), but it makes it easy to do a full (and correct) refactoring very quickly. Think of it as docs being part of the interface and the compiler forcing you to always update the docs. That's an amazing property
> So I do not quite see how this does not limit refactoring?
Yes it limits refactoring. But all languages provide syntactic and semantic constraints you must operate within. You can't just add line noise to a C program and expect it to compile.
So your complaint isn't that there are limits, it's that there is less of them in C, so it's easier to make changes in C without putting too much thought into it. That is absolutely correct. It's also true that's is far easier to introduce a bug in C code when you refactor than it is in Rust, and that's so because it's harder to write buggy code in Rust that gets past the compiler.
What does this code do? In C or C++, it's impossible to answer. If i overflows, it's UB. If you go past the end of the array, it's UB. Since it's inlined, there is no way to know if either could happen. If compiler can prove some particular instance is UB, it can do whatever it damned well pleases, without informing the poor programmer. There are lots of worse examples, particularly in C++.
In Rust, it's entirely predictable what happens for any given input, as Rust forbids UB in safe code. But in order to pull that off, Rust forces you to re-write the above function, perhaps into something like this:
or many other variations depending what you actually need to do. Notice for example you where forced to say whether you're happy with incrementing 255 to 256. If you weren't, you would write it as (i + 1) as usize.
Yes, it's more mental effort. In return, you don't get your arse handed to you on a platter because you recompiled with a newer, smarter version of the compiler that noticed you violated some language rule only a language lawyer would know, and took advantage of it.
> A line of code does not depend on a million other things.
But it does! To qoute my top-level comment:
> What about about race conditions, null pointers indirectly propagated into functions that don't expect null, aliased pointers indirectly propagated into `restrict` functions, and the other non-local UB causes?
In other words: you set some pointer to NULL, this is OK in that part of your program, but then the value travels across layers, you've skipped a NULL check somewhere in one of those layers, NULL crosses that boundary and causes UB in a function that doesn't expect NULL. And then that UB itself also manifests in weird non-local effects!
Rust fixes this by making nullability (and many other things, such as thread-safety) an explicit type property that's visible and force-checked on every layer.
Although, I agree that things like macros and trait resolution ("overloading") can be sometimes hard to reason about. But this is offset by the fact that they are still deterministic and knowable (albeit complex)
This is true to some degree, but not really that much in practice. When refactoring you just add assertions for NULL (and the effect of derefencing a NULL in practice is a trap - that it UB in the spec is completely irrelevant. in fact it helps because it allows compilers to turn it a trap without requiring it on weak platforms). Restrict is certainly dangerous, but also rarely used and a clear warning sign, compare it to "unsafe". The practical issues in C are bounds checking and use-after-free. Bounds checking I usually refactor into safe code quickly. Use-after-free are the one area where Rust has a clear advantage.
I agree that in practice NULL checking is one of the easier problems. I used it because it's the most obvious and easy to understand. I can't claim which kinds of unsoundness in C code are more common and problematic in practice. But that's not even interesting, given that safe Rust (and other safe languages) solve every kind of unsoundness.
> in fact it helps because it allows compilers to turn it a trap without requiring it on weak platforms
The "shared xor mutable" rule in Rust also helps the compiler a lot. It basically allows it to automatically insert `restrict` everywhere. The resulting IR is easier to auto-vectorize and you don't need to micro-optimize so often (although, sometimes you do when it comes to eliminating bound checks or stack copies)
> Restrict is certainly dangerous, but also rarely used and a clear warning sign, compare it to "unsafe".
It's NOT a clear warning sign, compared to `unsafe`. To call an unsafe function, the caller needs to explicitly enter an `unsafe` block. But calling a `restrict` function looks just like any normal function call. It's easy to miss an a code review or when upgrading the library that provides the function. That the problem with C and C++, really. The `unsafe` distinction is too useful to omit.
You are not wrong, but also not really right. In practice, I never had a problem with "restrict". And this is my point about Rust being overengineered. While it solves real problems, you need to exaggerate the practical problems in C to justify its complexity.
> In practice, I never had a problem with "restrict".
That's exactly because it's too dangerous and the developers quickly learn to avoid it instead of using it where appropriate! Same with multithreading. C leaves a lot of optimization on the table by making the available tools too dangerous and forcing people to avoid them altogether.
Do you have any general, comprehensive benchmarks or statistics that would indicate the opposite? I would include one if I had one at hand, because that would be a stronger argument! But I'm not aware of such benchmarks. I have to cherry pick individual projects. I don't want to.
I still claim that, as a general trend, Rust replacements are faster while also being less bug-prone and taking much less time to write. Another such example is ripgrep.
You're focusing purely on the problem-fixing aspects, but Rust's expressiveness is why I like it.
C can't match that. In C, you're basically acting as a human compiler, writing lots of code that could be generated if you used a more expressive language. Plus, as has been mentioned, it supports refactoring easily and safely better than any language outside of the Haskell/ML space.
The advantages of Rust are a package which includes safety, expressiveness, refactoring support. You don't need to exaggerate anything for that package to make sense.
> This is true to some degree, but not really that much in practice. When refactoring you just add assertions for NULL (and the effect of derefencing a NULL in practice is a trap - that it UB in the spec is completely irrelevant.
This happens a lot in discussion about programming complexity. What you are doing is changing the original problem to a much simpler one.
Consider a parsing function parse(string) -> Option<Object>
This is the original problem, "Write a parsing function that may or may not return Object"
What a lot of people do is they sidetrack this problem and solve a much "simpler problem". They instead write parse(string) -> Object
Which "appears" to be simpler but when you probe further, they handwave the "Option" part to just, "well it just crashes and die".
This is the same problem with exceptions, a function "appears" to be simple: parse(string) -> Object but you don't see the myriads of exceptions that will get thrown by the function.
I guess it depends on what the problem is. If the problem is being productive and being able to program without pain "changing the original problem to a much simpler one" is a very good thing. And "crash and die" can be a completely acceptable way to deal with it. Rust's "let it panic" is not at all different in this respect.
But in the end, you can write Option just fine it C. I agree though that C sometimes can not express things perfectly in the type system. But I do not agree that this is crucial for solving these problems. And then, also Rust can not express everything in the type system. (And finally, there are things C can express but Rust can't).
> in the end, you can write Option just fine it C.
No, you can't. In the sense that the compiler doesn't have exhaustiveness checks and can't stop you from accessing the wrong variant of a union. An Option in C would be the same as manually written documentation that doesn't guarantee anything.
std::optional in C++ is the same too. Used operator* or operator-> on a null value? Too bad, instant UB for you. It's laughably bad, given that C++ has tools to express tagged unions in a more reliable way.
> And then, also Rust can not express everything in the type system. (And finally, there are things C can express but Rust can't).
That's true, but nobody claims otherwise. It's just that, in practice, checked tagged unions are a single simple feature that allows you to express most things that you care about. There's no excuse for not having those in a modern language.
And part of the problem is that tagged unions are very hard to retrofit into legacy languages that have null, exceptions, uninitialized memory, and so on. And wouldn't provide the full benefit even if they could be retrofitted without changing these things.
Yes, all I am saying is that we have to be honest what level of problems we are solving when we encounter a complicated solution.
The solution have to scale linearly with the problem at hand, that is what it means to have a good solution.
I agree with the article that Rust is overkill for most use cases. For most projects, just use a GC and be done with it.
> But I do not agree that this is crucial for solving these problems. And then, also Rust can not express everything in the type system.
This can be taken as a feature. For example, is there a good reason this is representable?
struct S s = 10;
I LOVE the fact that Rust does not let me get away with half-ass things. Of course, this is just a preference. Half of my coding time is writing one-off Python scripts for analysis and data processing, I would not want to write those in Rust.
> But in the end, you can write Option just fine it C.
Even this question have a deeper question underneath it. What do you mean by "just fine"? Because to me, tagged enums or NULL is NOT the same thing as algebraic data types.
This is like saying floating points are just fine for me for integer calculations.
Maybe, maybe for you its fine to use floating points to calculate pointers, but for others it is not.
>When refactoring you just add assertions for NULL
This is a line of thinking I used to see commonly when dynamic typing was all the rage. I think the difference comes from people who view primarily work on projects where they are the sole engineer vs ones where they work n+1 other engineers.
"just add assertions" only works if you can also sit on the shoulder of everyone else who is touching the code, otherwise all it takes is for someone to come back from vacation, missing the refactor, to push some code that causes a NULL pointer dereference in an esoteric branch in a month. I'd rather the compiler just catch it.
Furthermore, expressive type systems are about communcation. The contracts between functions. Your CPU doesn't case about types - types are for humans. IMO you have simply moved the complexity from the language into my brain.
This was about refactoring a code base where a type is assumed to be non-null but it is not obvious. You can express also in C on interfaces that a pointer is non-null.
> I do not really agree with respect to C. I often have to deal with C code written by unexperienced programmers. It is always relatively easy to refactor it step by step.
Really? How confident are you to change a data structure that uses an array with linear search lookup to a dictionary? Or a pointer that now is nullable (or is now never null)?
Unless you have rigorous test or the code is something trivial, this would be a project of its own.
I am pretty sure I can swap out the implementation of the dictionary in the rust compiler and by the time the compilation issues are worked out, the code would be correct by the end of it (even before running the tests)
I can compare programming and refactoring large code bases in C and C++, Rust, and Python, from system and parsing and protocol libraries to asynchronous multi-threaded servers in the mentioned languages.
Refactoring Rust projects is clearly the easiest because the compiler and type system ensure the program is correct at least in terms of memory access and shared resource access. It doesn't protect me from memory leaks and logical errors. But since Rust has a built-in testing framework, it's quite easy to prepare tests for logical errors before refactoring.
C/C++ refactoring is a nightmare - especially in older projects without modern smart pointers. Every change in ownership or object lifetime is a potential disaster. In multi-threaded applications it's even worse - race conditions and use-after-free bugs only manifest at runtime, often only in production. You have to rely on external tools like Valgrind or AddressSanitizer.
Python has the opposite problem - too much flexibility. You can refactor an entire class, run tests, everything passes, but then in production you discover that some code was dynamically accessing an attribute you renamed. Type hints help, but they're not enforced at runtime.
Rust forces you to solve all these problems at compile time. When you change a lifetime or ownership, the compiler tells you exactly where you need to fix it. This is especially noticeable in async code - in C++ you can easily create a dangling reference to a stack variable that an async function uses. In Rust, it simply won't compile.
The only thing where Rust lags a bit is compilation speed during large refactors. But that's a small price to pay for the certainty that your code is memory-safe.
Another area where Rust absolutely excels is when using AI agents like Claude Code. It seems to me that LLMs can work excellently with Rust programs, and thanks to the support of the type system and compiler, you can get to functional code quickly. For example, Claude Code can analyze Rust programs very well and generate documentation and tests.
I think Rust with an AI agent has the following advantages:
Explicit contract - the type system enforces clear function interfaces. The AI agent knows exactly what a function expects and what it returns.
Compiler as collaborator - when AI generates code with an error, it gets a specific error message with the exact location and often a suggested solution. This creates an efficient feedback loop.
Ownership is explicit - AI doesn't have to guess who owns data and how long it lives. In C++ you often need to know project conventions ("here we return a raw pointer, but the caller must not deallocate it").
Fewer implicit assumptions - in Python, AI can generate code that works for specific input but fails on another type. Rust catches these cases at compile time.
Reading between the lines, the author is a Haskell fan. Haskell is another "complicated" language, but the complexity feels much different than the C++ complexity. Perhaps I would describe it as "complexity that improves expressiveness". If you like Haskell for its expressiveness but dislike C++ for it's complexity, I suspect Rust is a language you're going to like.
My impression was the opposite. I wondered how well the author knew Haskell. They mention:
(1) The "intimidating syntax". Hey, you do not even need to be using <$> never mind the rest of those operators. Perl and Haskell can be baroque, but stay away from that part of the language until it is useful.
(2) "Changes are not localized". I'm not sure what this means. Haskell's use of functions is very similar to other languages. I would instead suggest referring to the difficulty of predicting the (time|space) complexity due to the default lazy evaluation.
FTA:
> In contrast, Haskell is not a simple language. The non-simplicity is at play both in the language itself, as evidenced by its intimidating syntax, but also in the source code artifacts written in it. Changes are not localized, the entire Haskell program is one whole — a giant equation that will spit out the answer you want, unlike a C program which is asked to plod there step by step.
> (1) The "intimidating syntax". Hey, you do not even need to be using <$> never mind the rest of those operators. Perl and Haskell can be baroque, but stay away from that part of the language until it is useful.
But you have to be able to read and understand code using them.
The two are incomparable in both quality and quantity. The complexity of Rust comes from the fact that it's solving complex problems. The complexity of C++ comes from a poorly thought out design and backwards-compatibility. (Not to slight the standards committee; they are smart people, and did the best with what they had.)
Anothere way of putting it is, if you didn't care about backwards-compatibility, you could greatly simplify C++ without losing anything. You can't say the same about Rust; the complexity of Rust is high-entropy, C++'s is low-entropy.
As someone that really enjoys C++, I would say that the current issues are cause by lack of preview implementations before being voted into the standard, this is just broken, but there are not enough people around to be able to turn the ship around.
Those folks eventually move to something else and adopt "C++ the good parts" instead.
For C++, just take lambda captures, you can do [&], [=], [a], [&a], [a = b] (and I probably forgot half), the many way you can do initialization in C++ became a meme, but it really permeates the whole language whose design is driven by enthusiasts and people who have an interest to make it more complex (trainers, book authors, consultants, etc.)
For Rust, I think it is a bit of a different story and it is harder to point to specific features. The language is clearly much better designed (because it was more designed and did not evolve so much) and because of its roots in functional programming. Just overall, the complexity is too high in my opinion and it a bit too idealistic and not pragmatic enough.
> it a bit too idealistic and not pragmatic enough
This is an ideal programming language for certain types of people. It also gives the programming language certain properties that make it useful when provable correctness is a concern (see Ferrocene).
Sorry, I didn't mean provable correctness as in using formal methods, I meant it in terms of far stronger compiler guarantees about especially things such as memory safety. I also personally find Rust code far more pleasant to write and to reason about compared to C/C++ because of how well-defined and consistent it is.
The language is highly productive, it’s pragmatic enough. There’s no perfect language until our brains can directly describe what we want a machine to do without intermediary translation layers like C.
The question was mainly meant to elicit examples for Rust (I asked about C++ just to get a baseline of what the original commenter thinks "overengineered" means), but nobody seems to have any for Rust :)
It's really not even remotely the same. C++ has literally >50 pages of specification on the topic of initialising values. All of these are inconsistent, not subject to any overarching or unifying rule, and you have to keep it all in mind to not run into bugs or problematic performance.
Rust doesn't need a formal document so badly, because (in safe code) the punishment for not following the rules is much more clear and small. It's either a logic bug that doesn't lead to memory-related vulnerabilities, or a readable compiler error. Of course, if your compiler doesn't catch and explain your errors to you, you need a separate book to help you with that!
To be fair, the formal document isn't for language users, it's for compiler implementors. Rust doesn't have a formal document for other reasons, mainly that there is one main Rust compiler, as opposed to C or C++ where there are dozens that all need to agree on behavior.
People over-index on "formal" here, the Rust reference and Ferrocene (which will end up being adopted as the official spec) is just as "formal" as the C++ specification.
There are other compilers in development, and they're able to coordinate with these documents. There is of course always more work to do, but it's really not as far away as some people believe.
International standards have nothing to do with formality in the computer science sense.
US corporations also have massive influence on the C and C++ specifications, just look at the brouhaha around Bloomberg and contracts for C++, for example.
And the Rust Foundation does not author the specification, the Rust Project does. So in many ways, companies on the Foundation board have less direct influence than the companies who send their employees as representative to WG21 or similar.
My use of formal was from: “The standard is not intended to teach how to use C++. Rather, it is an international treaty – a formal, legal, and sometimes mind-numbingly detailed technical document intended primarily for people writing C++ compilers and standard library implementations.”
Swift is a great C++ and Rust alternative that doesn’t get enough attention outside of Apple platforms. It’s a performant, statically typed, compiled language that feels almost like a scripting language to write code in. It’s memory safe, cross platform, has a fantastic standard library, and has excellent concurrency capabilities. Even the non-Xcode tooling is maturing rapidly.
The big weak spot really is lack of community outside of Apple platforms.
I would love to see a cross-platform desktop UI toolkit for Swift, preferably one that’s reactive and imperative-dominant with declaritivity sprinkled in where it makes sense (all-in declarative design like SwiftUI hits too many language weak points for the time being). Swift is really quite nice to write once you get a feel for it, and as long as one is judicious about advanced feature use, it looks more familiar and less intimidating than Rust does which is great for newcomers.
Swift is coming to Android. That may improve traction. [1]
Having just ported a small library from Rust to Swift, it’s fairly clear that Rust is ridiculously fast but Swift is just so much more readable. Much easier to debug too.
Also - why would Rust only have a max-heap? In the library I ported, the authors had to reverse the algorithmic logic in the entire library for this.
Even though it’s only about half as fast as Rust, Swift performance does start to shine with concurrency.
> Also - why would Rust only have a max-heap? In the library I ported, the authors had to reverse the algorithmic logic in the entire library for this.
Depending on how you define “ergonomics,” I guess. I think only needing to provide a comparison function is pretty elegant, rather than duplicating the data structure. YMMV.
Having developed a fair amount of expertise with both C++ and Rust, C++ is on a completely different level of complexity from Rust.
In Rust, for most users, the main source of complexity is struggling with the borrow checker, especially because you're likely to go through a phase where you're yelling at the borrow checker for complaining that your code violates lifetime rules when it clearly doesn't (only to work it out yourself and realize that, in fact, the compiler was right and you were wrong) [1]. Beyond this, the main issues I run into are Rust's auto-Deref seeming to kick in somewhat at random making me unsure of where I need to be explicit (but at least the error messages basically always tell you what the right answer is when you get it wrong) and to a much lesser degree issues around getting dyn traits working correctly.
By contrast C++ has just so much weird stuff. There's three or four subtly different kinds of initialization going on, and three or four subtly different kinds of type inference going on. You get things like `friend X;` and `friend class X;` having different meanings. Move semantics via rvalue references are clearly bolted on after the fact, and it's somewhat hard to reason about the right things to do. It has things like most-vexing parse. Understanding C++ better doesn't give you more confidence that things are correct; it gives you more trepidation as you know better how things can go awry.
[1] And the commonality of people going through this phase makes me skeptical of people who argue that you don't need the compiler bonking you on the head because the rules are easy to follow.
The C++ 11 move semantic is definitely an example of C++ programmers being sold a "pig in a poke"† The claim in the proposal document was that although this isn't the "destructive move" which programmers wanted (and which Rust had by 2015), the C++ 11 move feature can be realised with less disruption and is just as good. In reality it left significant performance on the table and you can't get it back without significant further language change.
† This phrase would have been idiomatic many years ago but it is still used with the same intent today even though its meaning is no longer obvious, the idea is that a farmer at market told you this sack you can't see inside ("poke") has a piglet in it, so you purchase the item for a good price, but it turns out there was only a kitten in the bag, which (compared to the piglet) is worthless.
Copy by default is the right way to go. Even if less performant, it’s safe and super-easy to understand. Let the people that want to squeeze the last drop of performance worry about moves…
Move by default is the thing which complicates Rust so much.
It's funny because I have completely the opposite stance. When I code in rust (mainly algorithm), I always struggle to change what I want to do to what rust allow me to do. And all this complexity has nothing to do with the problem.
>If there is a language that can rival C++ in terms of complexity
Fair, but this relative. C++ has 50 years of baggage it needs to support--and IMO the real complexity of C++ isn't the language, it's the ecosystem around it.