In my experience (20+ years with C/C++, and about 4 years with Rust), Rust is significantly less complex than C++, while being similarly capable. The extra syntax that throws off so many C++ devs is almost exclusively about data types and lifetimes, which I find very useful for understanding my own code and others', and which I wish I had in C++.
Some of this is just knowing from experience, by the time C++ programmers knew they wanted the destructive move assignment semantic at the turn of the century they already had large codebases which relied on C++ copy assignment, so, too bad. It took a significant extra effort to land C++ 11 move semantics, which are still less useful but also have worse ergonomics. Whereas Rust knew it wanted the destructive move so, that's just how everything works in Rust.
But there are a bunch of unforced errors in C++ design beyond that. Default implicit conversion is a choice and a mistake. Multiple inheritance is a mistake, Stroustrup even says he did it because it was easy, which is exactly the same cause as Hoare's NULL. Choosing "All correct programs compile" (the other option was "No incorrect programs compile", you can't have both, see Henry Rice's PhD thesis) was a mistake. My favourite bad default in C++ is atomic memory ordering. The nasty trick here was that picking a default was the mistake, it's not that they picked the wrong one but that they picked a default at all. C++ programmers end up writing code which doesn't specify the ordering even though the ordering was their only important decision.
Agreed. C++ has advanced incredibly over the years, with the developers putting in immense, important, and useful effort. But Rust has had the benefit of a fresh start with lessons learned. People who have yet to understand the choices made in it's design see the differing semantics as unnecessary hurdles, whereas people who've taken the time to learn why those choices were made and what adhering to them enables find themselves enamored of their newfound abilities. It's why there's such an intense communication rift between folks on either side of the experience.
> Choosing "All correct programs compile" (the other option was "No incorrect programs compile", you can't have both …
This is really the important distinction between C++ and Rust.
In my opinion, it seems easier to complement the former to catch issues afterwards (like this article) than it is to design a language that does not require you to jump through hoops to get something correct to compile.
I hope programming language design progresses to a state that makes my point invalid, but the “bro rust is easier than C++” gaslighting culture does not help.
I'm quite experienced at C++ and not that experienced in Rust... but I believe that writing correct Rust is easier than writing correct C++. People get C++ to compile alright, but it often has problems at runtime. You need to know what you are doing in both, but C++ allows you to compile with certain classes of bugs anyway. Even experts still occasionally introduce bugs in C++ that Rust wouldn't allow.
> In my opinion, it seems easier to complement the former to catch issues afterwards (like this article)
Fil-C of course can't magically fix your incorrect program. It never had any defined meaning, but the compiled executable does something and Fil-C will ensure that if the thing it does involves say, a use-after-free at runtime now it exits reporting the error, but it can't fix the fact it's nonsense, that's not their purview.
There's no point in hoping that somehow Programming Languages will overturn Mathematics. I mean, I can't blame you for trying, Bjarne Stroustrup is a professor and still seems to think that should be attempted, but it's futile. We're definitely talking "Why can't I extinguish the sun with water?" level thinking.
Obviously I can't speak to your own experience but for me certainly Rust is easier than C++.
> There's no point in hoping that somehow Programming Languages will overturn Mathematics.
Maybe you misunderstood my point?
Getting rust to a more complete state would be overturning mathematics here, as you note you can’t have both soundness and completeness.
What I say does not require overturning mathematics, ie allow unsound programs to compile but have different methods of catching them, both statically or dynamically.
Rice's Theorem says we can advance arbitrarily close but can't reach the goal of compiling exactly the set of correct programs (all correct, none incorrect). Some years ago Rust landed "Non-lexical lifetimes" borrow checking which is an example of such an advance, you don't need to overturn mathematics to make such advances, only to reach the goal. Work to further improve lifetime checking is ongoing though I doubt anything as big as NLL is on the foreseeable horizon.
The problem isn't directly with C++ choosing "All correct programs compile" but instead with the resulting incentive structure. Programmers want their program to compile.
In Rust the incentive is to improve the compiler, allowing more programs (all of them correct) to compile as with the NLL changes.
But in C++ the incentive is to loosen the requirements, allowing more programs (some of them incorrect) to compile, as with Concepts Lite in 2020.
Rust has unsafe for the purpose of not requiring a magic compiler for all cases. It's not above-and-beyond the notion of augmenting an unsafe language with tooling; it just shifts the default mode of safety.
-Wall/-Werror/-Wextra have existed for a long, long time. -Wno-error will still work in gcc15, which quickly defeats the point of werror being a default. If your're writing c/c++ in 2026 and not using wall/werror/wextra, you're actively choosing to compile unreliable code and you should have been jumping through those "hoops" the whole time.
"But, all my dependencies!" Fork them and fix the warnings. Flow the changes back upstream. Use your fork if they're not accepted. Don't buy libraries that compile with warnings, or demand your vendor fix them.
I've been writing c/c++ for a long, long time. The bugs that actually bite you are almost always logical and have little to do with things like a borrow-checker, ask cloudflare.
> I like the concepts proposed by Rust but do not like fighting with the borrow checker or sprinkling code with box, ref, cell, rc, refcell, etc.
I'm not sure why this would be confusing or disliked by a C++ dev.
Rust's Box<T> is similar to C++'s std::unique_ptr<T>.
Rust's Rc<T>, Arc<T>, and Rc<RefCell<T>> serve similar uses to C++'s std::shared_ptr<T>.
Rust's Weak<T> is similar to C++'s std::weak_ptr<T>.
Verbosity of both is nearly identical. The big difference is that Rust enforces the rules around aliasing and mutability at compile time, whereas with C++ I get to find out I've made a mistake when my running code crashes.
std::unique_ptr<T> is almost exactly Option<Box<T>>
The Option is important, Rust's Box<T> is always a boxed T, but std::unique_ptr<T> might not be a boxed T, it might be "disengaged" and there isn't a T
C++ move operations are thus closest to Rust's core::mem::take function, they not only move something, they also need to always replace it with some empty default state, in Rust's case specifically Default::default. Box<T> may not implement Default, but Option does so unconditionally, because its default is always just None.
You may find when converting some code that you didn't want Option<Box<T>> but only Box<T> because in fact you always have a boxed T here - it's never disengaged, and if you do that then Rust's type system has helped in a small way to clarify your code so that's nice.
Reminds me of “A monad is a monoid in the category of endofunctors, what's the problem?”
As you write this, do you not start to see why this would be confusing?
Yes, C++ is pretty bad at this too.
The fact that these exist and the programmer has to always be consciously be aware of it is an indication that something has gone wrong in the language design.
Imagine if you were to write C code in 1970, but you always had to keep track of which registers each variable corresponded to. That is how I look at these.
(There were, indeed, early 'high level' languages that required you to do this :)
> The facts that these exist and the programmer has to always be consciously be aware of it
This is what separates a systems programming language suitable for OS and embedded development from managed languages, which are not.
The complexity ultimately stems from unavoidable details of the hardware. Languages which do not offer similar representations will be incapable of making full use of the underlying hardware, including writing certain projects like bootloaders, firmwares, OSes, etc. Rust is pretty close to state-of-the-art in terms of providing reasonable abstractions over the hardware.
It sounds to me like you're used to managed languages with a runtime which are great for certain applications, but unable to be specific enough about memory layout, how data is formatted in memory, etc. for some tasks. Those choices were made for you by the language runtime's authors and necessarily limit the language's applicability to some problems. Rust, C++, and other systems programming languages don't have such limitations, but require you to understand more of the complexity of what the system is actually doing.
Any language which provides adequate representations for taking full advantage of the hardware is going to be on a similar order of complexity as Rust, C++, or other systems programming languages, because the hardware is complex. Managed languages can be nice for introducing folks to programming, precisely because much of the complexity is hidden in the runtime, but that can be a double edged sword when it comes time to approach a systems level task.
Instead of wishing the language didn't offer those representations, it may be more productive to ask why C++ and Rust converged on such similar ones. Exploration of that question will be enlightening.
None of these have anything to do to do with memory layout or how data is formatted in the memory. You are arguing a different point than the one being discussed here.
These are fundamentally hacks around the compiler’s inability to understand ownership and lifetimes, at least the way Rust (and C++) are designed.
These exist in Rust because otherwise you would have to use unsafe blocks all the time to write any reasonable code.
Safe threading seems like fully exploiting hardware features to me. That's the biggest thing smart pointers and Rc, Arc, and Cell types enable. Perhaps I could have been clearer.
The comment about memory layout was an additional point that dealing with hardware requires that you format, align, and position information in memory as the hardware expects, which requires either exposing those details, or encoding them in a runtime.
Safe threading sounds great, as long as the language gets out of my way when I absolutely do not care about safe threading.
I write compilers (and have contributed to the Rust compiler!), and there has been approximately zero times in twenty or so years that thread safety has been a concern.
I don’t buy at all that you’ve coded in a language like C++ and thread safety has never been a concern. Either:
1. You work on esoterically simple problems where nothing is worth threading (implied by you questioning whether it bought any meaningful performance).
2. You code in languages like Rust where it’s not a problem or significantly less of one (Go, Java, c#, Haskell, Ocaml, etc).
You’re being awfully dismissive of people who’s experiences don’t match yours, especially when it seems like your experience isn’t the norm.
Whether you use raw threads or not in no way changes that thread safety is completely independent of that; eg a work stealing queue of a fixed thread pool still can have issues. And high performance code often needs you to implement certain constructs which means someone has to implement them and be sure they’re safe.
Also I’ve worked on a lot of different codebases, from iOS at Apple to Android at Meta, from machine learning to VR video streaming. Claiming that people aren’t using raw threads in C++ a) isn’t borne out by the evidence b) is ignoring the challenges of writing thread safe code that has nothing to do with manually creating a thread. Hell, my team hit a libc++ bug in std::conditional_var.
> Thread safety has not usually been a concern because it’s pretty rare to use raw threads in C++. Rust folks always seem to assume people write C++ like it is 1995.
What kind of charmed life do you lead to where use of reliable abstractions over threads is common in C++ codebases you touch?
I write a bit of everything, including a little Rust CAD library which is a continuation of work I did in C++ 20 years ago. The C++ version was never multithreaded. The Rust library was fully multithreaded in an afternoon by importing Rayon behind a cargo feature, and using par_iter in place of iter in a few hot loops. That alone made the rewrite worth it.
That afternoon effort on the hot loops netted ~3x improvement on the test suite on an 8 core machine. The test suite is intentionally minimal, so I'd say that represents a reasonable low bound. The greater the number of polygons in an operation, the more it would benefit.
There should be no such difference. The bigger problem is that OS's enforce this duality when in fact there should only be application level software and an absolutely tiny core to handle IPC and scheduling. This then allows you to enforce the boundaries between various bits far more strictly.
The kernel, the memory manager and device drivers, the language runtimes for managed languages; would these not be written in "systems" languages? Essential complexity can only be rearranged, not removed. Nothing changes the fact that every interface between hardware and software needs to be bridged.
On a different note: why should the kernel even handle IPC or scheduling? Those take the basic capabilities of context switching, timer management, and memory management. Even "core functionality" can be a context switch (or a few) away; is a syscall not just a message to a system server? Only the most basic form of communication is necessary to delegate arbitrary functionality, so a true microkernel should only introduce that as an abstraction. Everything else either follows from the hardware or is left to the whims of software.
> The kernel, the memory manager and device drivers, the language runtimes for managed languages; would these not be written in "systems" languages?
The kernel and memory manager: probably yes, the device drivers: not necessarily, the language runtimes for managed languages: not necessarily.
> On a different note: why should the kernel even handle IPC or scheduling?
Because any other solution will quickly run into chicken-and-the-egg style problems.
> Those take the basic capabilities of context switching, timer management, and memory management.
Yes. But only one timer, the rest should be free to use by other applications.
> Even "core functionality" can be a context switch (or a few) away; is a syscall not just a message to a system server?
No. A syscall is usually defined as a call to a ring one level in from the one where you currently are. But lots of things that are syscalls right now do not necessarily have to be.
> Only the most basic form of communication is necessary to delegate arbitrary functionality, so a true microkernel should only introduce that as an abstraction.
Indeed. And they do.
> Everything else either follows from the hardware or is left to the whims of software.
A lot of the stuff that 'follows from the hardware' can be dealt with at the application level.
> The kernel and memory manager: probably yes, the device drivers: not necessarily, the language runtimes for managed languages: not necessarily.
The parts that touch hardware (or similarly bare kernel interfaces) must be so. Sure, you could split device driver implementations and so on, but somewhere there's a meaningful lower level of software within the system.
> Because any other solution will quickly run into chicken-and-the-egg style problems.
No. The kernel must provide for context switching. It would be like migrating threads IPC, but one-way. No threads, no scheduler, no dedicated data transfer. In other words, the bare minimum necessary to make a sensible abstraction around switching processes.
seL4, according to its developers, is not absolutely a microkernel. I believe the rationale mainly points to the in-kernel scheduler, but seL4's IPC interacts with the scheduler and is noticeably more elaborate than a mere context switch. Even if seL4's IPC is, by most standards, minimal, I do not consider it to be so objectively. I described a meaningfully more minimal alternative.
Delegating scheduling to userspace is trivial. If necessary, designate a scheduler to run if no scheduling decision is available. It has been done before, and the only usual objection is performance.
> No. A syscall is usually defined as a call to a ring one level in from the one where you currently are. But lots of things that are syscalls right now do not necessarily have to be.
Just as hardware interrupts can be abstracted into messages, syscalls can be abstracted into messages. I'm not saying that the hardware implementation directly conforms to the abstraction.
> The parts that touch hardware (or similarly bare kernel interfaces) must be so.
That's just a choice and no you are incorrect. It is perfectly possible to deal with hardware in managed languages.
> Sure, you could split device driver implementations and so on, but somewhere there's a meaningful lower level of software within the system.
This is optional. Been there, done that. Many times.
> No. The kernel must provide for context switching. It would be like migrating threads IPC, but one-way. No threads, no scheduler, no dedicated data transfer. In other words, the bare minimum necessary to make a sensible abstraction around switching processes.
Yes, IPC is context switching. Timer interrupts can cause extra context switches.
> seL4, according to its developers, is not absolutely a microkernel. I believe the rationale mainly points to the in-kernel scheduler, but seL4's IPC interacts with the scheduler and is noticeably more elaborate than a mere context switch. Even if seL4's IPC is, by most standards, minimal, I do not consider it to be so objectively. I described a meaningfully more minimal alternative.
It is a pretty poor implementation in my opinion.
> Delegating scheduling to userspace is trivial. If necessary, designate a scheduler to run if no scheduling decision is available. It has been done before, and the only usual objection is performance.
It's trivial, except for the little problem that your userspace program can be killed and then you have no scheduler.
> Just as hardware interrupts can be abstracted into messages, syscalls can be abstracted into messages. I'm not saying that the hardware implementation directly conforms to the abstraction.
Ok. If you want to use a different description of the term syscall than is common then that's fine but you should define that up front. Your definition of a syscall simply does not match mine.
> It is perfectly possible to deal with hardware in managed languages.
Perhaps we're using the words in different ways. What I mean is: in order to interact with something, it must either be done directly or through abstraction. If abstraction is used, it must be realized without itself. A language such as Java or C#, at least for performance, must expose some things on a lower level that the runtime normally abstracts away. (Or, say, the OS memory manager can't use virtual memory before it enables virtual memory.) Technically, the lower level could be programmed with a dialect of Java/C# or something like that, or there may be another level of abstraction that hides the details (a compiler generating code in the background, for example). But something has to be the first step at the bottom.
> Yes, IPC is context switching.
(Considering the diversity of how OSes approach context switches/IPC, I use "context switch" to mean "CPU mode/address space switch", more or less.)
Usually, IPC is context switching and overhead. seL4's IPC has overhead involved with scheduling, threads, and message buffering. My design isolates the context switching, which is common to all IPC designs.
> Timer interrupts can cause extra context switches.
I don't see what your point is. I assure you that my system is not uniquely fragile to timer interrupts.
> It's trivial, except for the little problem that your userspace program can be killed and then you have no scheduler.
Then don't let the scheduler be killed. What is the difference between a privileged userspace program and a kernelspace program? Isn't that what microkernels demonstrate? Again, this has been done before.
> Ok. If you want to use a different description of the term syscall than is common then that's fine but you should define that up front. Your definition of a syscall simply does not match mine.
It wasn't quite a definition, though I did write poorly. In the abstract, a syscall can be seen as a message. If IPC ("to a process") is done by putting some data somewhere, setting a few registers to special values, and executing a magic syscall instruction, then a normal syscall can be considered IPC "to the kernel".
It seems like you think that some shadowy cabal somewhere decided to differentiate systems languages from managed languages and keep them divided. That is not the case. The distinction describes how the language has been implemented, which is based on the choices of the language authors alone, and is usually down to practical considerations about how to implement the thing at all.
Saying "There should be no such difference." is a bit like saying bicycles should be allowed on the highway and semi trucks should be accepted on walking paths. The difference is inherent in the thing. A result of how they were built. And what they can and can't accomplish as a result.
> It seems like you think that some shadowy cabal somewhere decided to differentiate systems languages from managed languages and keep them divided. That is not the case.
You could have made that point without the strawman, and what a ridiculous thing to say anyway.
> The distinction describes how the language has been implemented, which is based on the choices of the language authors alone, and is usually down to practical considerations about how to implement the thing at all.
That is so obvious I do not understand what point you are trying to make here.
> Saying "There should be no such difference." is a bit like saying bicycles should be allowed on the highway and semi trucks should be accepted on walking paths. The difference is inherent in the thing. A result of how they were built. And what they can and can't accomplish as a result.
No, the error is yours: you are interpreting my sentence in a way that is blatantly wrong and then argue with the outcome. I'm not saying that there shouldn't be 'trucks or bicycles' in terms of programming languages. What I'm saying is that the boundary between where you use a 'systems programming language' and where you use an 'application programming language' is artificial and that we are using too much of the former in a place where we probably should be using the latter.
> What I'm saying is that the boundary between where you use a 'systems programming language' and where you use an 'application programming language' is artificial
Well, everything humans have ever created is artificial, so I'm not sure even how to parse this sentence. There are objectively tasks which can be accomplished with systems languages which cannot be with others. That is an indisputable fact, and not determined by anything but the language specification and implementation.
> we are using too much of the former in a place where we probably should be using the latter
If I loosen this sentence to mean that we should write more code in languages which have the familiar properties of applications languages - memory and thread safety, low boilerplate, helpful tooling, reduced cognitive load - then I can agree with it. Otherwise it seems an opinion without reason.
Where that code needs speed, uninterrupted control of execution, to be embedded, target wasm, or might want to do any of those things in the future, I think Rust is an excellent choice which provides many of those benefits.
I'd rather not have runtimes sneaking into low level performance critical areas like kernels and drivers. But some folks are into that sort of thing - Microsoft famously worked on https://en.wikipedia.org/wiki/Singularity_%28operating_syste... in C# but then again C# has very similar memory semantics to Rust. Microkernels seem to work well enough, with several extant examples. Anyway, you do you! I look forward to ogling your OS project from the sidelines.
You've now spent 12 (more?) comments in a thread about a language that isn't rust talking about rust. On top of that there are multiple bad takes, strawmen, purposeful misunderstandings and a whole heap of text that has zero bearing to the topic at hand. I'm going to mute your account now because I'd much rather read about the actual topic than the one that you seem to want to drag in here. If you ever wonder why people get a bit tired of Rust advocates, this is why.
All replies to a parent comment about Rust which wasn't mine, and which you're responding under too. Heaven forbid folks converse freely.
> If you ever wonder why people get a bit tired of Rust advocates, this is why.
I'm no Rust advocate. Just a developer who's worked in the languages being discussed, who is happy to talk about them. If I'm an advocate for anything, it's more sophisticated, powerful, safer, better tooling, in every language. I appreciate it when any of them make advancements.
> Great, if you are right everyone is going to be using Rust eventually.
Every task does not need speed and safety. Therefore, "everyone" doesn't need Rust.
But I could easily see a future where C++ is relegated to legacy language status. It has already had decades of garbage-collected languages chipping away at most of its general-purpose uses, but Rust seems capable and in a position to take away most of its remaining niches.
It's kind of why the old C++ programmer that I am decided to learn Rust in the first place - seemed like a good idea at the time to skate where the puck is heading.
I hope you realize how your critique is identical to the critiques people have about IPv6
Is your claim that the borrow checker is the problem? That’s a really difficult design space to beat Rust in:
1. Shared mutable data structures to allow high performance code
2. No GC to allow high performance code
3. Non lexical lifetimes to allow flexibility in memory ownership for expressivity and performance (ie you can’t restrict allocations to never escape a lexical lifetime)
Capability based systems might avoid the Rust borrow checker but they’re even more verbose with annotations and complex than Rust.
Effects systems show some promise but not yet proven they can actually be a general purpose language like Rust (ie do the ideas scale well to multiple different problem domains).
Anyway, there’s lots of alternative ways of designing languages but they all come with undesirable tradeoffs. Would be better if you actually made some concrete proposals that Rust gets wrong rather than “it’s too complex - they should have made it simpler” without taking any position - always easier to critique from the sidelines without making a proposal of your own that can be critiqued.
this seems somewhat unlikely to me. My guess is that we'll see a continued split where Rust takes the low level like OS kernels and cryptography that need time consistency and adversarial security guarantees, while most higher level programs (apps/databases etc) are written in fast garage collected languages (e.g C#/Julia).
> Rust is literally the fast stab at making a memory safe systems language.
Graydon started developing Rust in 2006, 20 years ago. The guy is somewhat famously a compiler buff with experience in a half dozen of them. What about that comes across as a "fast stab" to you?
> Do you really think we are never going to be able to design a better one?
I didn't want to learn C++. I learned it anyway because it was the best in the niches I was interested in, as well as there being existing programs that I wanted to contribute to that were written in C++.
I can state for a certainty that if I had waited around for the perfect language before making the leap, it would have negatively affected both my career and hobbyist dalliances.
Did I mention waiting around? You should always use the right tool for the job, and right now if you need a memory safe systems programming language for a greenfield project, Rust is the tool.
My point is that Rust's ergonomic issues will see it replaced when someone figures out a way to net similar advantages in a friendlier language.
It's amusing to see strong beliefs on this being impossible!
I really like Rust, and I'll gladly accept if a language that advances its benefits appears. Just as Rust is available to avoid using C++, that language would be available to avoid using Rust. It's not a competition; it's pragmatism.
The real answer should have been a new language that has memory safety without all the extra conceptual changes and orthogonal subsystems that Rust brings. The core value of safety did not need the reinvention of everything else with the accompanying complexity and cognitive load. For example Zig which instead of introducing a new metaprogramkming language, it uses...... Zig - imagine using the same language instead of inventing a new additional language with all the accompanying complexity and cognitive load and problems. Rust is for those who revel in complexity. And traits - traits and extra complexity not needed for safety. And result and option and move by dedfault - none of these things were needed but they all add up to more complexity and unfamiliarity and cognitive load. And when you add it all together and intertwine it you end up with something so unfamiliar that it no longer looks like "ordinary programming" it looks like something from the Cambrian period.
As a C++ developer, my experience with learning both Rust and Zig is that they're both good languages, and any reasonably skilled C++ developer could learn either language if they put their mind to it.
If you forced me to pick between Zig and Rust for a long-running project though, I'd pick Rust 10/10 times for the simple fact that it has been stable for more than a decade and already has momentum and funding behind it. Zig is a cool language - one that I've actually written more of than Rust - but it hasn't hit 1.0 yet and still has significant churn in both the language and standard library.
That is not a statement anyone can deny. Unless you're a Rust-bro "hey man I learned it so I’m baffled how you can't see it's super simple ..... etc etc" - often the implication that you're not very smart if you think Rust is complex.
Of everything that I have learned about programming, this is the BIGGEST lesson: - complexity is bad, avoid complexity. Complex is not the same as "sophisticated", which implies necessary intricacy. Complex means unneeded unnecessary cognitive load - things made harder than they should be when it could have been avoided - that's complexity. If you are writing complex code then you're writing bad code. And Rust is complex.
It is sad that we did not end up with a SIMPLE programming language that solves the memory safety problem.
What we needed was Rust-- i.e Rust without all the non-safety related extras that make it different - it's all the non-safety add ons that makes Rust into a Rube Goldberg machine.
> It is sad that we did not end up with a SIMPLE programming language that solves the memory safety problem.
I am not a savant by any means, and yet I was able to get up and running with Rust relatively quickly. What Rust isn't is ergonomic, in that Rust gets very annoyed with the ways that one might want to structure their code. Trust me, I got bit by the borrow checker countless times, and it did grate on me.
As a result, there are many tasks that I would avoid using Rust for, tasks where both speed and safety aren't critical. But if neither is a priority, the list of alternative languages I can resort to is quite long, much longer than Zig, C++, or C. And in the cases where both are a factor, I would consider being needled by the compiler to be a feature and not a bug.
> It is sad that we did not end up with a SIMPLE programming language that solves the memory safety problem.
We did, arguably. Those languages are called JavaScript, Python, Java, C#, etc. Those languages tend to be eschewed in certain niches, though, and it's there that simplicity tends to be harder to achieve.
> What we needed was Rust-- i.e Rust without all the non-safety related extras that make it different - it's all the non-safety add ons that makes Rust into a Rube Goldberg machine.
I think it might also be worth considering that some people find those "non-safety related extras" a good thing. Dropping backwards compatibility and/or familiarity, just like everything else, is a tradeoff, and that tradeoff might be worth it if you think the resulting semantics are nicer to work with.
You focused on the C++ aspect and completely failed to engage with the actual critique - what is a “simple” language that you’re evaluating Rust against as a failure?
You'd be surprised to see, after deep inspection, how little of Rust you can remove while keeping its safety story the same (that is, memory safe without GC).
Traits? Nope. We need some way for code reuse. Classes cannot be made memory safe without extra cost (at least, I don't know how can they). And they are not less complex either. Templates like C++? More complex, and doesn't allow defining safety interfaces. No tool for code reuse? That will also severely limit the safety (imagine how safe Rust was if everyone would need to roll their `Vec`).
The borrow checker of course cannot be omitted. ADTs are really required for almost anything Rust does (and also, fantastic on their own). Destructors? Required to prevent use after free.
Async can be removed (and in fact, wasn't there in the beginning) which is a large surface area, but even today it can mostly be avoided if you're not working in some areas.
I don't think anybody can deny Rust is complex, but most often it's inherent complexity (what you call "sophistication") given the constraints Rust operates in, not accidental complexity.
Absolutely this. Folks are used to an awful lot of the complexity being hidden from them through avoidance of threading, runtimes, garbage checkers, standard libraries, and so on. For a language which exposes all of the complexity, Rust feels minimalist. C++ is one of a small number of other languages which also expose all the complexity, and it feels gargantuan and like poorly-thought out additions after additions by comparison. I don't mean to disparage the C++ devs at all, C++ has managed to be useful for ~40 years, and it's still capable of incredible things. Just that we've learned a lot over those 40 years, and computational capacity has grown significantly, and Rust has had the opportunity and architecture to integrate some of that learning more fundamentally.
Somehow most of the libraries in the Rust ecosystem seem to interoperate with each other seamlessly, and use the same build system, which I didn't have to learn another unrelated language to use! Astounding!
Says who? You can totally do code reuse using manually-written dynamic dispatch in "rust without traits". That's how C does it, and it works just fine (in fact, it's often faster than Rust's monomorphic approach that results in a huge amount of code bloat that is often very unfriendly to the icache).
Granted, a lot of safety features depend on traits today (send/sync for instance) but traits is a much more powerful and complex feature than you need for all of this. It seems to me like it's absolutely possible to create a simpler language than Rust that retains its borrow checker and thread safety capabilities.
Now whether that'd be a better language is up to individual taste. I personally much prefer Rust's expressiveness. But not all of it is necessary if your goal is only "get the same memory and thread safety guarantees".
> Says who? You can totally do code reuse using manually-written dynamic dispatch in "rust without traits". That's how C does it, and it works just fine.
Rust can monomorphize functions when you pass in types that adhere to specific traits. This is super-handy, because it avoids a bounce through a pointer.
The C++ equivalent would be a templated function call with concept-enforced constraints, which was only well-supported as of C++20 (!!!) and requires you to move your code into a header or module.
Zig can monomorphize with comptime, but the lack of trait-based constraint mechanism means you either write your own constraints by hand with reflection or rely on duck typing.
C doesn't monomorphize at all, unless you count preprocessor hacks.
Isn't Zig's repetitive ceremonial code around allocators+ allocation + defer *.deinit() a sign of a serious shortcoming like golang's error handling? If zig is so good at metaprogramming, why isn't there a metaprogramming solution to this repetitive code?
Memory allocations are always done explicitly (nothing is hidden or implicit). I've not written enough Zig yet to appreciate that, but I've hit plenty of those issues year-after-year with C++ to know their approach is sane and rational.
That's like saying Rust has GC because GC libraries/runtimes can be implemented in/for Rust. Rust recognizes allocations on a language basis, but does not provide the same level of control over allocations on a language basis as Zig does. For instance, there is no stable interface for custom allocators for alloc/std.
std may not yet provide a stable interface for custom allocators (the Allocator API appears to be available in unstable presently), but use of custom allocators is common in no-std rust environments like embedded. Reading through https://github.com/irbull/custom_allocators the Allocator API doesn't seem particularly complicated. I think it's fair to expect that it will stabilize in time.
> That's like saying Rust has GC because GC libraries/runtimes can be implemented in/for Rust.
Quite a few have already been implemented and are available as libraries exactly as you describe, today. I wouldn't phrase that as "Rust has GC" because that might imply that the GC is required. But if your application might benefit from GC, it's certainly available in the language. I might say "GC is optionally available in Rust if desired" to be more accurate.
> I think it's fair to expect that it will stabilize in time.
The allocator work is facing a lot of obstacles, unfortunately. I prefer it to be unstable for as long as it needs, though.
> Quite a few have already been implemented and are available as libraries exactly as you describe, today. I wouldn't phrase that as "Rust has GC" because that might imply that the GC is required.
This is exactly my point. Rust does not prevent GC by any means, but Rust also does not encourage GC by any means. The same goes for custom allocators, aside from the highly-unstable Allocator API.
> The allocator work is facing a lot of obstacles, unfortunately.
Got any links? Sounds like interesting reading. Seriously. Kind of thing I come here for. I'd really appreciate it. Or I can ask the AI for some.
> I prefer it to be unstable for as long as it needs, though.
Sure, same. Fully baking things is a process, and it's nice when things are fully baked. So I agree. I think Rust's async could be a bit more ergonomic, though it wasn't too difficult to wrap my head around, and sort of shockingly simple to implement a basic no-std async executor (~10 lines), so maybe I'm coming around. I was pleased to find out that it was simple enough that I could do it, and understand how it worked, as I try to do with all my microcontroller code, and wasn't dependent on a big async runtime like Tokio if I didn't need it's features (or bloat).
No, it isn't. Because that distinction is significant if you are using the language in an environment where those libraries are not available or suitable, such as the Linux project which uses a custom fork of Alloc which provides collections for different allocators.
So, that is not done at the language level but the library level. Unless the compiler is modified for Linux, but even if it is, that's entirely bespoke and unstable. This is not comparable to Zig's design. I'm aware that anything can be done if the right things are implemented; I'm talking about what Rust-the-language currently does for control over allocation. If the answer comes down to "we're doing something custom", then the language is soewhat or largely sidestepped. C-the-language certainly doesn't have panics or exceptions, even though longjmp or a custom runtime could be used.
For the time being, I consider alloc and std to be part of the language, and the compiler also has provisions for them if they are used. If the alternative is supposed to be only using core, then the language does not provide for control over allocations the way Zig does. Imposing allocations with no control and not providing allocations at all are both failure modes. With enough effort, anyone could do anything, but programming languages exist to control and enhance certain things under their purview. Rust does not have a comparable facility to control allocations like it controls ownership and borrowing. Just as C-with-tooling being safe isn't the same as C being safe, Rust-with-libraries providing control over allocations isn't the same as Rust providing control over allocations.
> The real answer should have been a new language that has memory safety without all the extra conceptual changes and orthogonal subsystems that Rust brings. The core value of safety did not need the reinvention of everything else with the accompanying complexity and cognitive load.
What would the minimal set of features be, in your opinion?
> For example Zig which instead of introducing a new metaprogramkming language, it uses...... Zig - imagine using the same language instead of inventing a new additional language with all the accompanying complexity and cognitive load and problems.
Zig probably isn't the best comparison since Zig doesn't try to achieve the same level of compile-time memory safety guarantees that Rust aims for. For instance, Zig doesn't try to statically prevent use-after-frees or data races.
That being said, as with everything it's a question of tradeoffs. Zig's metaprogramming approach is certainly interesting, but from what I understand it doesn't offer the same set of features as Rust's approach. For example:
- Zig's generics are more similar to C++ templates in that only instantiated functions are fully checked by the compiler. Rust's generics, on the other hand, are completely checked at the definition site so if the definition type-checks the author knows it will type-check for all possible instantiations. Rust's approach also lends itself to nicer error messages since everything a generic needs is visible up front.
- Zig's comptime isn't quite 1:1 with Rust's macros. comptime is for... well, compile-time computation (e.g., reflection, compile-time branching, or instantiating types). Macros are for manipulating syntax (e.g., code generation or adding inline support for other languages). Each has things the other can't do, though to be fair there is overlap in problems they can be used to solve.
In any case, metaprogramming approaches are (mostly?) independent of memory safety.
> And result and option and move by dedfault - none of these things were needed but they all add up to more complexity and unfamiliarity and cognitive load.
I don't think Result/Option are that complex (if at all) since they're trivially derivable from discriminated unions/sum types/enums.
I'm also not sure how move by default is necessarily "more complexity... and cognitive load"? Maybe as a result of unfamiliarity, perhaps, but that seems more a property of a person than a language, no?
There's also https://docs.rs/proc-macro2/latest/proc_macro2/ which effectively allows for the construction of procedural macros with normal Rust possible. As well as enable the use of proc_macro types in normal rust. It'll make it to stable someday, I hope.
> The real answer should have been a new language that has memory safety without all the extra conceptual changes and orthogonal subsystems that Rust brings.
So what you're saying here is that you don't understand that Rust's rules around memory ownership, aliasing, and mutability are what allow the language to provide deterministic compile time memory safety without runtime cost. If you figure out another way to guarantee memory safety at compile time with zero runtime overhead, you should write a paper and start another language around it!
And even if you a runtime solution with no runtime cost, you'd still need to run the code, to find the memory safety bugs. Static analysis is supposed to tell you there is no path that violates memory safety.
> In my experience (20+ years with C/C++, and about 4 years with Rust), Rust is significantly less complex than C++, while being similarly capable
OK, what about if you have an existing code base of hundreds of thousands, or even millions, of lines of C or C++? Still think it's going to be better, and especially cheaper, to "rewrite it in Rust" instead of just porting it to Fil-C or something similar?
Oh I'm all for pragmatism. I think Fil-C is great in that regard. For shoring up legacy code and maybe even for new projects if you're a C/C++ shop and velocity is important and you can eat the performance and memory costs.
I've also written some bindings for C/C++ in Rust and it couldn't have been simpler. bindgen is amazing. So with the experience I have today, I wouldn't hesitate to start rewriting portions of a C/C++ project in Rust where it made sense. And keep the thing building before, during, and after.
I'm currently in process on a rewrite of the RepRap.org stack I helped write 20 years ago in C++ and avr-c++, Python, OpenSCAD, and other languages to idiomatic Rust in order to reduce the number of languages involved, support building a fast wasm user interface, build the interface and firmware in the same language and share types across the boundary, explore GPU compute, enable multithreading of interface and firmware, and explore async firmware architecture. And I'm enjoying it a lot.
Compared to writing it the first time, I am spending zero time fighting build systems, able to reuse much more code from libraries much easier, and have been able to delve deeper into complex issues like advanced numerical representations, ensuring algorithmic determinism under all possible cases, and the implementations of vector math library which is the foundation of much of the code. Even egui, the UI toolkit I've been using, seems simpler and easier to work with than QT or GTK or anything I've worked with save FLTK which was dead simple. I've really enjoyed Rust for all that.
I have the sense, after working with rust, that I am much less likely to get myself into trouble with it than with C++, and much more likely to know where that might happen. Past some point, my big C++ projects got big enough that it got difficult to hold a coherent picture of the whole thing at runtime in your head all at once, to understand it's behavior. That point seems to be farther out, for me, in Rust.
The most challenging thing about the rewrite has been chasing down all the little bits of state hidden away in various C++ functions, and normalizing all that into one of a few architectures which work well with Rust's borrowing rules. But the process of doing so consists entirely of things which are best-practice anyway, and the resulting code is definitely easier to read and understand to my eye.
These are just observations from my position, as a person with a big legacy C++ codebase, currently rewriting in Rust. And occasionally working in other languages, of course. I have lots of projects.
I don’t know what you mean by SafER but it’s important to remember that Fil-C sacrifices a lot of performance for that safety which detracts the reasons you’d be running that software as otherwise C was a bad language for them. Sometimes this won’t matter but there are places fil-c won’t be able to go that Rust can - embedded and OS kernel come to mind. Other places would be things like browsers or games. Rust gives you the safety without giving up the ability to do performance.
Also, I could be wrong but I believe any assembly linked into Fil-C bypassed the safety guarantees which would be something to keep in mind (not a big deal generally, but a source of hidden implicit unsafe).
I’m apparently comment happy on this OP, but, the typing of it looks funny because it starts the sentence, I’m pretty sure OP was saying safER, as opposed to SAFE (as in totally safe instead of comparatively safer). I have been quite charitable to OP in some sibling comments and will do so here. I think OP is attempting to give Fil-C some credit for being an attempt to increase the overall memory safety of existing code without incurring the complexity of a new language or the complexity of rewriting long running/widely distributed code. It is a decent sentiment and a viable methodology to achieve a laudable goal, but is certainly susceptible to caveats like the performance penalty you mention.
I’m going to start this comment by specifying that I don’t know what OP was considering complex about Rust and, unfortunately, a large amount of discussion on the topic tends toward strawman-ing by people looking to argue the ‘anti-Rust’ side of said discussions. Additionally, the lack of a contextual and well considered position against some aspect of Rust, as a language, is very common, and at worst the negative take is really just a overall confrontational stance against Rust’s uptick in usage broadly and its community of users, as perceived (and also strawmanned), in a generally negative light. But since borrowing is not explicitly mentioned by GP, I will give a slightly different position than he might, but I think this is an interesting perspective difference to discuss and not a blatant ad hom argument used to ‘fight’ Rust users on the internet.
From my position the complexity incurred by ownership semantics in Rust does not stem from Rust’s ‘formalization’ and semi-reification of a particular view on ownership as a means of program constraint. The complexity of Rust, in relation to ownership, comes with the lengths I would have to go to design systems using other logical means of handling references (particularly plain hardware implemented pointers) to semantic objects: their creation, specification, and their deletion. Additionally, other means of handling resources (particularly memory acquired via allocation): its acquisition, transport through local and distributed processes (from different cores to over the wire), and its deletion or handing back to OS.
Rust adopts ownership semantics (and value semantics to a large degree) to the maximum extent possible and has enmeshed those semantics throughout all levels of abstraction in the language definition (as far as a singular authoritative ‘definition’ can be said to exist as a non-implementation related formalism). At the level of Rust the language, not merely discussions and discourse about the language, ownership semantics are baked in at a specified granularity and that ownership is compositional over the abstraction mechanisms provided. These semantics dictate how everything from a single variable on the stack to large size allocations in a general heap to non-memory ‘resources’, like files, textures, databases, and general processes, are handled in a program. On top of the ownership semantics sit the rest of Rust’s semantics and they are all checked at compile time by a singular oracular subsystem (i.e. the borrow checker).
The complexity really begins to rise, for me, if ai want to attempt to program without engaging with ownership as the methodology or semantics for handling all of the above mentioned ‘resources’. I prefer, and believe, that a broader set of formalisms should be available for ‘handling’ resources, that those formalisms should exist with parameterized granularity, and that the foundational semantics for those mechanisms should come from type systems’ ability to encode capabilities and conditions for particular types in a program. That position is in contrast to the universal and foundational ownership semantic, especially with the individualistic fixed granularity, that Rust chose.
That being said, it is bordering on insanity to attempt to program in such a ‘style’/paradigm/method in Rust. My preferences make Rust’s chosen focus on ownership seem complex at the outset, and attempts to try and impose an alternate formalism in Rust (which would, by necessity, have to try and be some abstraction over Rust’s ownership semantics which hid those semantics and tried to present a different set of semantics to thenprogrammer) take that complexity to even higher levels.
The real problem with trying to frame my position here as complexity is the following: to me Rust and its ownership semantic is complex because I do not like it’s chosen core semantic construct, so when I think about achieving something using Rust I have to deal with additional semantics, semantic objects, and their constraints on my program that I do not think are fit for purpose. But, if I wanted to program in Rust without trying to circumvent, ignore, or disregard it’s choices as a language and just decided to accept (or embrace) it’s semantic choices the complexity I perceive would decrease significantly and immediately.
For me, Rust’s ownership semantics create an impedance mismatch that at the level of language use FEELS like complexity (and acts like complexity in a lot of ways), but is probably more correctly identified as just what it is… an impedance mismatch, nothing more and nothing less. For me, I just chose not to use Rust to avoid that, but for others they get focused on these issues and don’t actually get to the bottom of their issues and just default to calling it complexity during discussion.
All in all, I am probably being entirely to optimistic about the comments about the complexity of Rust and ownership and most commenters are just fighting to fight, but I genuinely believe there is much to discuss and work through in programming language design theory and writing walls of text on HN helps me do that.
Rust's strict ownership model isn't flexible enough sometimes (particularly when dealing with hardware concepts), so I consider it a "mostly good enough" abstraction that is a good default but should be circumvented sometimes.
I agree that your previous comment could be shorter, maybe half as long, with some effort. For example, this sentence could just be removed without changing the content or tone:
> But I think this is an interesting perspective difference to discuss and not a blatant ad hom argument used to ‘fight’ Rust users on the internet.
(It is already clear that you share your perspective because you think that it's interesting and not a fallacy.)
Then, there are some paragraphs close to the end that seem to repeat ideas.
But IMHO it's a good, nuanced comment that both addresses shortcomings in current discourse and adds ideas to the discussion. This is obviously difficult to do in few words.
A few concepts that come to mind from your comment, but I missed from others:
- Are Rust-style ownership semantics complex or just hard for me? Is it an issue of familiarity only? Is it ergonomics?
- Are they hard in general or just due to the way I like to program? How can I change my style to better fit the model?
- Are they even fit for the software I write? i.e. is it worth it to change how I program to better fit the model?
- What other tools are there to deal with resources?
- What could a programming language do to offer multiple of those? How can we mix GC, region types, linear/uniqueness types, manual management, etc. in a single language?
- A bunch of stuff about discourse in this thread and HN/the internet in general (which is maybe not the point of the comment and could be omitted?)
I think it might be an interesting experiment to try to duplicate as much of the functionality of IKOS as possible in vanilla Rust using a no_panic-like [0] technique. My guess is that most of the checks are already done by Rust or can be covered by such a technique, albeit perhaps with more hand-holding than for IKOS. The pointer alignment and comparison checks are the ones I'm least certain about since Rust is relatively lax with those.
I’m pretty sure there is not any realistically feasible way to ever prove your statement. But I hope a majority of people can recognize the sheer magnitude of C++ as a language and take a position that it may not be possible to master the whole thing. Rust is ‘smaller’ language using some metrics (most metrics really) than C++ is another thing I would hope most people can accept. So, given that the comparisons between the two wholes being a semi-intractable discussion I would propose the following:
When considering some chosen subset of functionality for some specified use case, how do Rust and C++ compare in the ability to ‘master’. There are wide and varied groups (practically infinite) of features, constructs, techniques, and implementations that achieve targeted use cases in both languages, so when constructing a given subset which language grants the most expressivity and capability in the more ‘tight’ (i.e. masterable) package?
I think that’s a way more interesting discussion to have. Obviously, where the specified use case requires Rust’s definition of memory safety to be implemented 100% of the time (excluding a small-ish percentage of delimited ‘unsafe but identifiable’ sections) the Rust subset will be smaller due to the mandatory abstractions required to put C++ anywhere near complete coverage. So it may make sense to allow the subset to be defined as not only constructs in the base language, but include sealed abstractions (philosophically if not in reality) as potential components in the constructed subsets.
I may have to try and formulate some use cases to pose in a longer something to see if any truly experienced devs can lay out their preferred language’s best candidate subset in response. It would also be fascinating to see what abstractions and metaprogramming would be used to implement the subset candidates and figure out how that could factor into an overall measurement of the ‘masterable-ness’ of the given language (i.e. how impossible a task is it to be able to rely on a subject matter expert to implement any proposed subset for any given use case).
It could be interesting to try to apply Fil-C techniques to Rust. After all, while safe Rust is fully memory safe and has many other forms of bug-resistance that Fil-C lacks, Fil-C is far safer than unsafe Rust. Maybe one could achieve both.
And there are probably a lot of optimizations available by skipping safety checks in safe Rust where the compiler could prove that the check was already done.
> It could be interesting to try to apply Fil-C techniques to Rust.
The bulk of the work Fil-C does is implemented as an LLVM pass [0, 1], so at least in principle it should be possible to get Rust code to compile using Fil-C as a "backend".
AFAIK Fil-C does not catch all memory safety bugs, for example some use-after-free are just not bugs but work as intended (you still access the original data/allocation). This means that it's not a sanitizer and code that runs fine on Fil-C may show UB when run normally.
> for example some use-after-free are just not bugs but work as intended (you still access the original data/allocation)
That doesn't sound right? For example, from the Fil-C GC docs [0]:
> If you call `free`, the runtime will flag the object as free and all subsequent accesses to the object will trap. Additionally, FUGC will not scan outgoing references from the object (since they cannot be accessed anymore).
eh, I daily-drive a -fsanitize=address -fsanitize=undefined build of Qt and actual memory bugs are almost never a thing - I think the only time I had some were in tooling executables such as qmllint, but not in the framework itself. Most of the bugs by large are more "behaviour" bugs.
ubsan definitely has some warnings due to a few "technically UB" patterns used as optimizations in Qt as they are known-working on every target platform under the sun, but yeah, no crashes :)
I'm only using this configuration for the software I develop though (+ libc++ debug mode) as it's painfully slow, but it exercises the Qt codebase in depth.
Depends in which sense you want it to "catch" the bugs. As this readme notes/quotes,
> All memory safety errors are caught as Fil-C panics.
If your problem is a memory-based bug causing a crash, I think this would just... catch the memory-based bug and crash. Like, it'd crash more reliably. On the other hand, if you want to find and debug the problem, that might be a good thing.
Sure, if the memory error is an immediately crashing one like a null per deref, but if is (for example) a memory corruption (e.g. an out of bounds write or a write-after-free) then this would be super helpful in exposing where those are happening at the source.
That’s what “catch” means here. As in, catch it in the act. Tools that make bugs crash more reliably and closer to the source of the problem are extremely valuable.
Afaict, there are some patterns that are not supported, like converting pointers to/from integers and doing stuff with them like bitmasks (which is a huge anti-pattern, but some code bases do it)
Most large C code bases aren’t really written in C. They’re written in an almost-C that includes certain extensions and undefined behavior. In this case, it uses inline assembly (an extension) and manipulating pointers as integers (undefined behavior).
While I’m always thankful when people give the broad perspective and context in a discussion, which your comment does. The specifics of this particular project’s usage of almost-C is not something I could have quickly figured out, so thanks. For such a large program, an to be as old as Qt is at this point, I find it impressive and slightly amazing that it has in some sense self-limited its divergence from standard C. It would be interesting to see what something like SQLite includes in its almost-C.
The more portable a project is, the less weird stuff it’s likely to do. The almost-C parts become more of a headache the more OSes and compilers you support. This seem pretty tame, and I’d expect SQLite to be similar. I work on some projects that only support a single OS, compiler, and CPU architecture and it’s full of dependencies on things like the OS’s actual address space (few 64-bit archs use all 64 bits).
My first job was on a large-ish software product that ran on several completely incompatible platforms - various Unixes, early Windows, IBM mainframes, etc - and window systems. At first, making all of them happy seemed like annoying busy-work.
But our code was extremely clean and extremely well-factored, because it had to be. And after porting our product to the first two or three new platforms, the later ones were much much easier to do. Lesson learned.
Since (say) the 1990s, it feels like "the world" has slowly converged to pretty much (a) the browser; (b) desktop computers - Windows, Linux, Mac; (c) phones/tablets; (d) everything else (mainframes, embedded, industrial, what have you - stuff that most people will never deal with). And portability across different platforms is no longer all that important. Which is fine, but I do miss how the need for portability forced us to work with discipline and be relentless on quality.
Massive projects like Qt also push compilers to their limits and use various compiler-specific and platform-specific techniques which might appear as bugs to Fil-C.
Sure fooled me. I follow his Twitter account and there isn't much he hasn't got building with it at this point. UX comes later. Amazing it's the random work of one person
The author wrote WebKit’s allocator and worked on JavaScriptCore for over a decade. I really enjoyed his posts on the WebKit blog over the years like this one on the concurrent garbage collector (2017) https://webkit.org/blog/7122/introducing-riptide-webkits-ret...
I don’t think so much is fil-c itself, but from the looks of the diff it’s a new platform essentially. That can require porting existing software generally which you can read from the posted diff
Interesting to see Fil‑C used with a large framework like Qt. The fact that it compiles with minimal changes says a lot about the compatibility layer and the InvisiCaps approach.
I believe much more in making C/C++ safer than using something as complex as Rust.
SafER is better than deeply complex and unable to be understood except by Rust experts.
In my experience (20+ years with C/C++, and about 4 years with Rust), Rust is significantly less complex than C++, while being similarly capable. The extra syntax that throws off so many C++ devs is almost exclusively about data types and lifetimes, which I find very useful for understanding my own code and others', and which I wish I had in C++.
Some of this is just knowing from experience, by the time C++ programmers knew they wanted the destructive move assignment semantic at the turn of the century they already had large codebases which relied on C++ copy assignment, so, too bad. It took a significant extra effort to land C++ 11 move semantics, which are still less useful but also have worse ergonomics. Whereas Rust knew it wanted the destructive move so, that's just how everything works in Rust.
But there are a bunch of unforced errors in C++ design beyond that. Default implicit conversion is a choice and a mistake. Multiple inheritance is a mistake, Stroustrup even says he did it because it was easy, which is exactly the same cause as Hoare's NULL. Choosing "All correct programs compile" (the other option was "No incorrect programs compile", you can't have both, see Henry Rice's PhD thesis) was a mistake. My favourite bad default in C++ is atomic memory ordering. The nasty trick here was that picking a default was the mistake, it's not that they picked the wrong one but that they picked a default at all. C++ programmers end up writing code which doesn't specify the ordering even though the ordering was their only important decision.
Agreed. C++ has advanced incredibly over the years, with the developers putting in immense, important, and useful effort. But Rust has had the benefit of a fresh start with lessons learned. People who have yet to understand the choices made in it's design see the differing semantics as unnecessary hurdles, whereas people who've taken the time to learn why those choices were made and what adhering to them enables find themselves enamored of their newfound abilities. It's why there's such an intense communication rift between folks on either side of the experience.
> Choosing "All correct programs compile" (the other option was "No incorrect programs compile", you can't have both …
This is really the important distinction between C++ and Rust.
In my opinion, it seems easier to complement the former to catch issues afterwards (like this article) than it is to design a language that does not require you to jump through hoops to get something correct to compile.
I hope programming language design progresses to a state that makes my point invalid, but the “bro rust is easier than C++” gaslighting culture does not help.
I'm quite experienced at C++ and not that experienced in Rust... but I believe that writing correct Rust is easier than writing correct C++. People get C++ to compile alright, but it often has problems at runtime. You need to know what you are doing in both, but C++ allows you to compile with certain classes of bugs anyway. Even experts still occasionally introduce bugs in C++ that Rust wouldn't allow.
You’re right.
That’s not what my point was about though.
> In my opinion, it seems easier to complement the former to catch issues afterwards (like this article)
Fil-C of course can't magically fix your incorrect program. It never had any defined meaning, but the compiled executable does something and Fil-C will ensure that if the thing it does involves say, a use-after-free at runtime now it exits reporting the error, but it can't fix the fact it's nonsense, that's not their purview.
There's no point in hoping that somehow Programming Languages will overturn Mathematics. I mean, I can't blame you for trying, Bjarne Stroustrup is a professor and still seems to think that should be attempted, but it's futile. We're definitely talking "Why can't I extinguish the sun with water?" level thinking.
Obviously I can't speak to your own experience but for me certainly Rust is easier than C++.
> There's no point in hoping that somehow Programming Languages will overturn Mathematics.
Maybe you misunderstood my point?
Getting rust to a more complete state would be overturning mathematics here, as you note you can’t have both soundness and completeness.
What I say does not require overturning mathematics, ie allow unsound programs to compile but have different methods of catching them, both statically or dynamically.
Rice's Theorem says we can advance arbitrarily close but can't reach the goal of compiling exactly the set of correct programs (all correct, none incorrect). Some years ago Rust landed "Non-lexical lifetimes" borrow checking which is an example of such an advance, you don't need to overturn mathematics to make such advances, only to reach the goal. Work to further improve lifetime checking is ongoing though I doubt anything as big as NLL is on the foreseeable horizon.
The problem isn't directly with C++ choosing "All correct programs compile" but instead with the resulting incentive structure. Programmers want their program to compile.
In Rust the incentive is to improve the compiler, allowing more programs (all of them correct) to compile as with the NLL changes.
But in C++ the incentive is to loosen the requirements, allowing more programs (some of them incorrect) to compile, as with Concepts Lite in 2020.
Rust has unsafe for the purpose of not requiring a magic compiler for all cases. It's not above-and-beyond the notion of augmenting an unsafe language with tooling; it just shifts the default mode of safety.
> than it is to design a language that does not require you to jump through hoops to get something correct to compile.
They are doing the same to C. With gcc 15 all warnings are treated as errors.
-Wall/-Werror/-Wextra have existed for a long, long time. -Wno-error will still work in gcc15, which quickly defeats the point of werror being a default. If your're writing c/c++ in 2026 and not using wall/werror/wextra, you're actively choosing to compile unreliable code and you should have been jumping through those "hoops" the whole time.
"But, all my dependencies!" Fork them and fix the warnings. Flow the changes back upstream. Use your fork if they're not accepted. Don't buy libraries that compile with warnings, or demand your vendor fix them.
I've been writing c/c++ for a long, long time. The bugs that actually bite you are almost always logical and have little to do with things like a borrow-checker, ask cloudflare.
> The bugs that actually bite you are almost always logical
Exactly right!
I would bet that the more time you spend fighting the programming language and compiler, the less you are thinking about complex logic.
Great, if you are right everyone is going to be using Rust eventually.
I like the concepts proposed by Rust but do not like fighting with the borrow checker or sprinkling code with box, ref, cell, rc, refcell, etc.
At some point there’s going to be a better designed language that makes these pain point go away.
> I like the concepts proposed by Rust but do not like fighting with the borrow checker or sprinkling code with box, ref, cell, rc, refcell, etc.
I'm not sure why this would be confusing or disliked by a C++ dev.
Rust's Box<T> is similar to C++'s std::unique_ptr<T>.
Rust's Rc<T>, Arc<T>, and Rc<RefCell<T>> serve similar uses to C++'s std::shared_ptr<T>.
Rust's Weak<T> is similar to C++'s std::weak_ptr<T>.
Verbosity of both is nearly identical. The big difference is that Rust enforces the rules around aliasing and mutability at compile time, whereas with C++ I get to find out I've made a mistake when my running code crashes.
std::unique_ptr<T> is almost exactly Option<Box<T>>
The Option is important, Rust's Box<T> is always a boxed T, but std::unique_ptr<T> might not be a boxed T, it might be "disengaged" and there isn't a T
C++ move operations are thus closest to Rust's core::mem::take function, they not only move something, they also need to always replace it with some empty default state, in Rust's case specifically Default::default. Box<T> may not implement Default, but Option does so unconditionally, because its default is always just None.
You may find when converting some code that you didn't want Option<Box<T>> but only Box<T> because in fact you always have a boxed T here - it's never disengaged, and if you do that then Rust's type system has helped in a small way to clarify your code so that's nice.
Reminds me of “A monad is a monoid in the category of endofunctors, what's the problem?”
As you write this, do you not start to see why this would be confusing?
Yes, C++ is pretty bad at this too.
The fact that these exist and the programmer has to always be consciously be aware of it is an indication that something has gone wrong in the language design.
Imagine if you were to write C code in 1970, but you always had to keep track of which registers each variable corresponded to. That is how I look at these.
(There were, indeed, early 'high level' languages that required you to do this :)
> The facts that these exist and the programmer has to always be consciously be aware of it
This is what separates a systems programming language suitable for OS and embedded development from managed languages, which are not.
The complexity ultimately stems from unavoidable details of the hardware. Languages which do not offer similar representations will be incapable of making full use of the underlying hardware, including writing certain projects like bootloaders, firmwares, OSes, etc. Rust is pretty close to state-of-the-art in terms of providing reasonable abstractions over the hardware.
It sounds to me like you're used to managed languages with a runtime which are great for certain applications, but unable to be specific enough about memory layout, how data is formatted in memory, etc. for some tasks. Those choices were made for you by the language runtime's authors and necessarily limit the language's applicability to some problems. Rust, C++, and other systems programming languages don't have such limitations, but require you to understand more of the complexity of what the system is actually doing.
Any language which provides adequate representations for taking full advantage of the hardware is going to be on a similar order of complexity as Rust, C++, or other systems programming languages, because the hardware is complex. Managed languages can be nice for introducing folks to programming, precisely because much of the complexity is hidden in the runtime, but that can be a double edged sword when it comes time to approach a systems level task.
Instead of wishing the language didn't offer those representations, it may be more productive to ask why C++ and Rust converged on such similar ones. Exploration of that question will be enlightening.
None of these have anything to do to do with memory layout or how data is formatted in the memory. You are arguing a different point than the one being discussed here.
These are fundamentally hacks around the compiler’s inability to understand ownership and lifetimes, at least the way Rust (and C++) are designed.
These exist in Rust because otherwise you would have to use unsafe blocks all the time to write any reasonable code.
Safe threading seems like fully exploiting hardware features to me. That's the biggest thing smart pointers and Rc, Arc, and Cell types enable. Perhaps I could have been clearer.
The comment about memory layout was an additional point that dealing with hardware requires that you format, align, and position information in memory as the hardware expects, which requires either exposing those details, or encoding them in a runtime.
Safe threading sounds great, as long as the language gets out of my way when I absolutely do not care about safe threading.
I write compilers (and have contributed to the Rust compiler!), and there has been approximately zero times in twenty or so years that thread safety has been a concern.
I don’t buy at all that you’ve coded in a language like C++ and thread safety has never been a concern. Either:
1. You work on esoterically simple problems where nothing is worth threading (implied by you questioning whether it bought any meaningful performance).
2. You code in languages like Rust where it’s not a problem or significantly less of one (Go, Java, c#, Haskell, Ocaml, etc).
You’re being awfully dismissive of people who’s experiences don’t match yours, especially when it seems like your experience isn’t the norm.
Thread safety has not usually been a concern because it’s pretty rare to use raw threads in C++.
Rust folks always seem to assume people write C++ like it is 1995.
> implied by you questioning whether it bought any meaningful performance
This is the first thing you should always ask when you do anything with parallelism! Are you familiar with Amdahl’s law?
Whether you use raw threads or not in no way changes that thread safety is completely independent of that; eg a work stealing queue of a fixed thread pool still can have issues. And high performance code often needs you to implement certain constructs which means someone has to implement them and be sure they’re safe.
Also I’ve worked on a lot of different codebases, from iOS at Apple to Android at Meta, from machine learning to VR video streaming. Claiming that people aren’t using raw threads in C++ a) isn’t borne out by the evidence b) is ignoring the challenges of writing thread safe code that has nothing to do with manually creating a thread. Hell, my team hit a libc++ bug in std::conditional_var.
IOS and related codebases at Apple are not using raw threads unless unavoidable.
> Thread safety has not usually been a concern because it’s pretty rare to use raw threads in C++. Rust folks always seem to assume people write C++ like it is 1995.
What kind of charmed life do you lead to where use of reliable abstractions over threads is common in C++ codebases you touch?
My experience has been the exact opposite.
> My experience has been the exact opposite
Okay, can you try and find a relatively modern large open source C++ codebase that actively uses raw threads?
I write a bit of everything, including a little Rust CAD library which is a continuation of work I did in C++ 20 years ago. The C++ version was never multithreaded. The Rust library was fully multithreaded in an afternoon by importing Rayon behind a cargo feature, and using par_iter in place of iter in a few hot loops. That alone made the rewrite worth it.
Great, how much faster was this multithreaded version for a real use case?
That afternoon effort on the hot loops netted ~3x improvement on the test suite on an 8 core machine. The test suite is intentionally minimal, so I'd say that represents a reasonable low bound. The greater the number of polygons in an operation, the more it would benefit.
There should be no such difference. The bigger problem is that OS's enforce this duality when in fact there should only be application level software and an absolutely tiny core to handle IPC and scheduling. This then allows you to enforce the boundaries between various bits far more strictly.
The kernel, the memory manager and device drivers, the language runtimes for managed languages; would these not be written in "systems" languages? Essential complexity can only be rearranged, not removed. Nothing changes the fact that every interface between hardware and software needs to be bridged.
On a different note: why should the kernel even handle IPC or scheduling? Those take the basic capabilities of context switching, timer management, and memory management. Even "core functionality" can be a context switch (or a few) away; is a syscall not just a message to a system server? Only the most basic form of communication is necessary to delegate arbitrary functionality, so a true microkernel should only introduce that as an abstraction. Everything else either follows from the hardware or is left to the whims of software.
> The kernel, the memory manager and device drivers, the language runtimes for managed languages; would these not be written in "systems" languages?
The kernel and memory manager: probably yes, the device drivers: not necessarily, the language runtimes for managed languages: not necessarily.
> On a different note: why should the kernel even handle IPC or scheduling?
Because any other solution will quickly run into chicken-and-the-egg style problems.
> Those take the basic capabilities of context switching, timer management, and memory management.
Yes. But only one timer, the rest should be free to use by other applications.
> Even "core functionality" can be a context switch (or a few) away; is a syscall not just a message to a system server?
No. A syscall is usually defined as a call to a ring one level in from the one where you currently are. But lots of things that are syscalls right now do not necessarily have to be.
> Only the most basic form of communication is necessary to delegate arbitrary functionality, so a true microkernel should only introduce that as an abstraction.
Indeed. And they do.
> Everything else either follows from the hardware or is left to the whims of software.
A lot of the stuff that 'follows from the hardware' can be dealt with at the application level.
> The kernel and memory manager: probably yes, the device drivers: not necessarily, the language runtimes for managed languages: not necessarily.
The parts that touch hardware (or similarly bare kernel interfaces) must be so. Sure, you could split device driver implementations and so on, but somewhere there's a meaningful lower level of software within the system.
> Because any other solution will quickly run into chicken-and-the-egg style problems.
No. The kernel must provide for context switching. It would be like migrating threads IPC, but one-way. No threads, no scheduler, no dedicated data transfer. In other words, the bare minimum necessary to make a sensible abstraction around switching processes.
seL4, according to its developers, is not absolutely a microkernel. I believe the rationale mainly points to the in-kernel scheduler, but seL4's IPC interacts with the scheduler and is noticeably more elaborate than a mere context switch. Even if seL4's IPC is, by most standards, minimal, I do not consider it to be so objectively. I described a meaningfully more minimal alternative.
Delegating scheduling to userspace is trivial. If necessary, designate a scheduler to run if no scheduling decision is available. It has been done before, and the only usual objection is performance.
> No. A syscall is usually defined as a call to a ring one level in from the one where you currently are. But lots of things that are syscalls right now do not necessarily have to be.
Just as hardware interrupts can be abstracted into messages, syscalls can be abstracted into messages. I'm not saying that the hardware implementation directly conforms to the abstraction.
> The parts that touch hardware (or similarly bare kernel interfaces) must be so.
That's just a choice and no you are incorrect. It is perfectly possible to deal with hardware in managed languages.
> Sure, you could split device driver implementations and so on, but somewhere there's a meaningful lower level of software within the system.
This is optional. Been there, done that. Many times.
> No. The kernel must provide for context switching. It would be like migrating threads IPC, but one-way. No threads, no scheduler, no dedicated data transfer. In other words, the bare minimum necessary to make a sensible abstraction around switching processes.
Yes, IPC is context switching. Timer interrupts can cause extra context switches.
> seL4, according to its developers, is not absolutely a microkernel. I believe the rationale mainly points to the in-kernel scheduler, but seL4's IPC interacts with the scheduler and is noticeably more elaborate than a mere context switch. Even if seL4's IPC is, by most standards, minimal, I do not consider it to be so objectively. I described a meaningfully more minimal alternative.
It is a pretty poor implementation in my opinion.
> Delegating scheduling to userspace is trivial. If necessary, designate a scheduler to run if no scheduling decision is available. It has been done before, and the only usual objection is performance.
It's trivial, except for the little problem that your userspace program can be killed and then you have no scheduler.
> Just as hardware interrupts can be abstracted into messages, syscalls can be abstracted into messages. I'm not saying that the hardware implementation directly conforms to the abstraction.
Ok. If you want to use a different description of the term syscall than is common then that's fine but you should define that up front. Your definition of a syscall simply does not match mine.
> It is perfectly possible to deal with hardware in managed languages.
Perhaps we're using the words in different ways. What I mean is: in order to interact with something, it must either be done directly or through abstraction. If abstraction is used, it must be realized without itself. A language such as Java or C#, at least for performance, must expose some things on a lower level that the runtime normally abstracts away. (Or, say, the OS memory manager can't use virtual memory before it enables virtual memory.) Technically, the lower level could be programmed with a dialect of Java/C# or something like that, or there may be another level of abstraction that hides the details (a compiler generating code in the background, for example). But something has to be the first step at the bottom.
> Yes, IPC is context switching.
(Considering the diversity of how OSes approach context switches/IPC, I use "context switch" to mean "CPU mode/address space switch", more or less.)
Usually, IPC is context switching and overhead. seL4's IPC has overhead involved with scheduling, threads, and message buffering. My design isolates the context switching, which is common to all IPC designs.
> Timer interrupts can cause extra context switches.
I don't see what your point is. I assure you that my system is not uniquely fragile to timer interrupts.
> It's trivial, except for the little problem that your userspace program can be killed and then you have no scheduler.
Then don't let the scheduler be killed. What is the difference between a privileged userspace program and a kernelspace program? Isn't that what microkernels demonstrate? Again, this has been done before.
> Ok. If you want to use a different description of the term syscall than is common then that's fine but you should define that up front. Your definition of a syscall simply does not match mine.
It wasn't quite a definition, though I did write poorly. In the abstract, a syscall can be seen as a message. If IPC ("to a process") is done by putting some data somewhere, setting a few registers to special values, and executing a magic syscall instruction, then a normal syscall can be considered IPC "to the kernel".
It seems like you think that some shadowy cabal somewhere decided to differentiate systems languages from managed languages and keep them divided. That is not the case. The distinction describes how the language has been implemented, which is based on the choices of the language authors alone, and is usually down to practical considerations about how to implement the thing at all.
Saying "There should be no such difference." is a bit like saying bicycles should be allowed on the highway and semi trucks should be accepted on walking paths. The difference is inherent in the thing. A result of how they were built. And what they can and can't accomplish as a result.
> It seems like you think that some shadowy cabal somewhere decided to differentiate systems languages from managed languages and keep them divided. That is not the case.
You could have made that point without the strawman, and what a ridiculous thing to say anyway.
> The distinction describes how the language has been implemented, which is based on the choices of the language authors alone, and is usually down to practical considerations about how to implement the thing at all.
That is so obvious I do not understand what point you are trying to make here.
> Saying "There should be no such difference." is a bit like saying bicycles should be allowed on the highway and semi trucks should be accepted on walking paths. The difference is inherent in the thing. A result of how they were built. And what they can and can't accomplish as a result.
No, the error is yours: you are interpreting my sentence in a way that is blatantly wrong and then argue with the outcome. I'm not saying that there shouldn't be 'trucks or bicycles' in terms of programming languages. What I'm saying is that the boundary between where you use a 'systems programming language' and where you use an 'application programming language' is artificial and that we are using too much of the former in a place where we probably should be using the latter.
> What I'm saying is that the boundary between where you use a 'systems programming language' and where you use an 'application programming language' is artificial
Well, everything humans have ever created is artificial, so I'm not sure even how to parse this sentence. There are objectively tasks which can be accomplished with systems languages which cannot be with others. That is an indisputable fact, and not determined by anything but the language specification and implementation.
> we are using too much of the former in a place where we probably should be using the latter
If I loosen this sentence to mean that we should write more code in languages which have the familiar properties of applications languages - memory and thread safety, low boilerplate, helpful tooling, reduced cognitive load - then I can agree with it. Otherwise it seems an opinion without reason.
Where that code needs speed, uninterrupted control of execution, to be embedded, target wasm, or might want to do any of those things in the future, I think Rust is an excellent choice which provides many of those benefits.
I'd rather not have runtimes sneaking into low level performance critical areas like kernels and drivers. But some folks are into that sort of thing - Microsoft famously worked on https://en.wikipedia.org/wiki/Singularity_%28operating_syste... in C# but then again C# has very similar memory semantics to Rust. Microkernels seem to work well enough, with several extant examples. Anyway, you do you! I look forward to ogling your OS project from the sidelines.
You've now spent 12 (more?) comments in a thread about a language that isn't rust talking about rust. On top of that there are multiple bad takes, strawmen, purposeful misunderstandings and a whole heap of text that has zero bearing to the topic at hand. I'm going to mute your account now because I'd much rather read about the actual topic than the one that you seem to want to drag in here. If you ever wonder why people get a bit tired of Rust advocates, this is why.
All replies to a parent comment about Rust which wasn't mine, and which you're responding under too. Heaven forbid folks converse freely.
> If you ever wonder why people get a bit tired of Rust advocates, this is why.
I'm no Rust advocate. Just a developer who's worked in the languages being discussed, who is happy to talk about them. If I'm an advocate for anything, it's more sophisticated, powerful, safer, better tooling, in every language. I appreciate it when any of them make advancements.
Have fun! Toodles!
> Great, if you are right everyone is going to be using Rust eventually.
Every task does not need speed and safety. Therefore, "everyone" doesn't need Rust.
But I could easily see a future where C++ is relegated to legacy language status. It has already had decades of garbage-collected languages chipping away at most of its general-purpose uses, but Rust seems capable and in a position to take away most of its remaining niches.
It's kind of why the old C++ programmer that I am decided to learn Rust in the first place - seemed like a good idea at the time to skate where the puck is heading.
> But I could easily see a future where C++ is relegated to legacy language status.
Yes, agreed. My prediction is that the replacement is a friendly language that makes Rust's ideas ergonomic to use.
I hope you realize how your critique is identical to the critiques people have about IPv6
Is your claim that the borrow checker is the problem? That’s a really difficult design space to beat Rust in:
1. Shared mutable data structures to allow high performance code
2. No GC to allow high performance code
3. Non lexical lifetimes to allow flexibility in memory ownership for expressivity and performance (ie you can’t restrict allocations to never escape a lexical lifetime)
Capability based systems might avoid the Rust borrow checker but they’re even more verbose with annotations and complex than Rust.
Effects systems show some promise but not yet proven they can actually be a general purpose language like Rust (ie do the ideas scale well to multiple different problem domains).
Anyway, there’s lots of alternative ways of designing languages but they all come with undesirable tradeoffs. Would be better if you actually made some concrete proposals that Rust gets wrong rather than “it’s too complex - they should have made it simpler” without taking any position - always easier to critique from the sidelines without making a proposal of your own that can be critiqued.
> That’s a really difficult design space to beat Rust in
You are right. My prediction is some new language does it in 5-10 years.
> identical to the critiques people have about IPv6
Maybe. With the difference that IPv6 gets out of your way when you do not want the advantages the new tech brings. (Rust? Noo.)
Even with that it is barely at 50% adoption after 20 years. https://www.google.com/intl/en/ipv6/statistics.html
this seems somewhat unlikely to me. My guess is that we'll see a continued split where Rust takes the low level like OS kernels and cryptography that need time consistency and adversarial security guarantees, while most higher level programs (apps/databases etc) are written in fast garage collected languages (e.g C#/Julia).
> this seems somewhat unlikely to me.
Rust is literally the fast stab at making a memory safe systems language.
Do you really think we are never going to be able to design a better one?
> Rust is literally the fast stab at making a memory safe systems language.
Graydon started developing Rust in 2006, 20 years ago. The guy is somewhat famously a compiler buff with experience in a half dozen of them. What about that comes across as a "fast stab" to you?
> Do you really think we are never going to be able to design a better one?
I didn't want to learn C++. I learned it anyway because it was the best in the niches I was interested in, as well as there being existing programs that I wanted to contribute to that were written in C++.
I can state for a certainty that if I had waited around for the perfect language before making the leap, it would have negatively affected both my career and hobbyist dalliances.
> I had waited around
Did I mention waiting around? You should always use the right tool for the job, and right now if you need a memory safe systems programming language for a greenfield project, Rust is the tool.
My point is that Rust's ergonomic issues will see it replaced when someone figures out a way to net similar advantages in a friendlier language.
It's amusing to see strong beliefs on this being impossible!
I really like Rust, and I'll gladly accept if a language that advances its benefits appears. Just as Rust is available to avoid using C++, that language would be available to avoid using Rust. It's not a competition; it's pragmatism.
> It's not a competition; it's pragmatism.
Exactly right
The real answer should have been a new language that has memory safety without all the extra conceptual changes and orthogonal subsystems that Rust brings. The core value of safety did not need the reinvention of everything else with the accompanying complexity and cognitive load. For example Zig which instead of introducing a new metaprogramkming language, it uses...... Zig - imagine using the same language instead of inventing a new additional language with all the accompanying complexity and cognitive load and problems. Rust is for those who revel in complexity. And traits - traits and extra complexity not needed for safety. And result and option and move by dedfault - none of these things were needed but they all add up to more complexity and unfamiliarity and cognitive load. And when you add it all together and intertwine it you end up with something so unfamiliar that it no longer looks like "ordinary programming" it looks like something from the Cambrian period.
As a C++ developer, my experience with learning both Rust and Zig is that they're both good languages, and any reasonably skilled C++ developer could learn either language if they put their mind to it.
If you forced me to pick between Zig and Rust for a long-running project though, I'd pick Rust 10/10 times for the simple fact that it has been stable for more than a decade and already has momentum and funding behind it. Zig is a cool language - one that I've actually written more of than Rust - but it hasn't hit 1.0 yet and still has significant churn in both the language and standard library.
My point is this:
Rust is complex.
That is not a statement anyone can deny. Unless you're a Rust-bro "hey man I learned it so I’m baffled how you can't see it's super simple ..... etc etc" - often the implication that you're not very smart if you think Rust is complex.
Of everything that I have learned about programming, this is the BIGGEST lesson: - complexity is bad, avoid complexity. Complex is not the same as "sophisticated", which implies necessary intricacy. Complex means unneeded unnecessary cognitive load - things made harder than they should be when it could have been avoided - that's complexity. If you are writing complex code then you're writing bad code. And Rust is complex.
It is sad that we did not end up with a SIMPLE programming language that solves the memory safety problem.
What we needed was Rust-- i.e Rust without all the non-safety related extras that make it different - it's all the non-safety add ons that makes Rust into a Rube Goldberg machine.
> It is sad that we did not end up with a SIMPLE programming language that solves the memory safety problem.
I am not a savant by any means, and yet I was able to get up and running with Rust relatively quickly. What Rust isn't is ergonomic, in that Rust gets very annoyed with the ways that one might want to structure their code. Trust me, I got bit by the borrow checker countless times, and it did grate on me.
As a result, there are many tasks that I would avoid using Rust for, tasks where both speed and safety aren't critical. But if neither is a priority, the list of alternative languages I can resort to is quite long, much longer than Zig, C++, or C. And in the cases where both are a factor, I would consider being needled by the compiler to be a feature and not a bug.
> It is sad that we did not end up with a SIMPLE programming language that solves the memory safety problem.
We did, arguably. Those languages are called JavaScript, Python, Java, C#, etc. Those languages tend to be eschewed in certain niches, though, and it's there that simplicity tends to be harder to achieve.
> What we needed was Rust-- i.e Rust without all the non-safety related extras that make it different - it's all the non-safety add ons that makes Rust into a Rube Goldberg machine.
I think it might also be worth considering that some people find those "non-safety related extras" a good thing. Dropping backwards compatibility and/or familiarity, just like everything else, is a tradeoff, and that tradeoff might be worth it if you think the resulting semantics are nicer to work with.
> Rust is complex.
Compared with what? C++? It is not.
False equivalence.
You focused on the C++ aspect and completely failed to engage with the actual critique - what is a “simple” language that you’re evaluating Rust against as a failure?
You'd be surprised to see, after deep inspection, how little of Rust you can remove while keeping its safety story the same (that is, memory safe without GC).
Traits? Nope. We need some way for code reuse. Classes cannot be made memory safe without extra cost (at least, I don't know how can they). And they are not less complex either. Templates like C++? More complex, and doesn't allow defining safety interfaces. No tool for code reuse? That will also severely limit the safety (imagine how safe Rust was if everyone would need to roll their `Vec`).
The borrow checker of course cannot be omitted. ADTs are really required for almost anything Rust does (and also, fantastic on their own). Destructors? Required to prevent use after free.
Async can be removed (and in fact, wasn't there in the beginning) which is a large surface area, but even today it can mostly be avoided if you're not working in some areas.
I don't think anybody can deny Rust is complex, but most often it's inherent complexity (what you call "sophistication") given the constraints Rust operates in, not accidental complexity.
Absolutely this. Folks are used to an awful lot of the complexity being hidden from them through avoidance of threading, runtimes, garbage checkers, standard libraries, and so on. For a language which exposes all of the complexity, Rust feels minimalist. C++ is one of a small number of other languages which also expose all the complexity, and it feels gargantuan and like poorly-thought out additions after additions by comparison. I don't mean to disparage the C++ devs at all, C++ has managed to be useful for ~40 years, and it's still capable of incredible things. Just that we've learned a lot over those 40 years, and computational capacity has grown significantly, and Rust has had the opportunity and architecture to integrate some of that learning more fundamentally.
Somehow most of the libraries in the Rust ecosystem seem to interoperate with each other seamlessly, and use the same build system, which I didn't have to learn another unrelated language to use! Astounding!
> Traits? Nope. We need some way for code reuse.
Says who? You can totally do code reuse using manually-written dynamic dispatch in "rust without traits". That's how C does it, and it works just fine (in fact, it's often faster than Rust's monomorphic approach that results in a huge amount of code bloat that is often very unfriendly to the icache).
Granted, a lot of safety features depend on traits today (send/sync for instance) but traits is a much more powerful and complex feature than you need for all of this. It seems to me like it's absolutely possible to create a simpler language than Rust that retains its borrow checker and thread safety capabilities.
Now whether that'd be a better language is up to individual taste. I personally much prefer Rust's expressiveness. But not all of it is necessary if your goal is only "get the same memory and thread safety guarantees".
> Says who? You can totally do code reuse using manually-written dynamic dispatch in "rust without traits". That's how C does it, and it works just fine.
Rust can monomorphize functions when you pass in types that adhere to specific traits. This is super-handy, because it avoids a bounce through a pointer.
The C++ equivalent would be a templated function call with concept-enforced constraints, which was only well-supported as of C++20 (!!!) and requires you to move your code into a header or module.
Zig can monomorphize with comptime, but the lack of trait-based constraint mechanism means you either write your own constraints by hand with reflection or rely on duck typing.
C doesn't monomorphize at all, unless you count preprocessor hacks.
Isn't Zig's repetitive ceremonial code around allocators+ allocation + defer *.deinit() a sign of a serious shortcoming like golang's error handling? If zig is so good at metaprogramming, why isn't there a metaprogramming solution to this repetitive code?
Memory allocations are always done explicitly (nothing is hidden or implicit). I've not written enough Zig yet to appreciate that, but I've hit plenty of those issues year-after-year with C++ to know their approach is sane and rational.
Memory allocations in Rust are also always done explicitly, but Rust's library types don't have APIs that allow you to get it wrong.
Rust doesn't (yet) have the same level of control over allocators as Zig does.
The Rust language has exactly that level of control. Rust's Alloc and STD implementations do not yet provide it.
That's like saying Rust has GC because GC libraries/runtimes can be implemented in/for Rust. Rust recognizes allocations on a language basis, but does not provide the same level of control over allocations on a language basis as Zig does. For instance, there is no stable interface for custom allocators for alloc/std.
std may not yet provide a stable interface for custom allocators (the Allocator API appears to be available in unstable presently), but use of custom allocators is common in no-std rust environments like embedded. Reading through https://github.com/irbull/custom_allocators the Allocator API doesn't seem particularly complicated. I think it's fair to expect that it will stabilize in time.
> That's like saying Rust has GC because GC libraries/runtimes can be implemented in/for Rust.
Quite a few have already been implemented and are available as libraries exactly as you describe, today. I wouldn't phrase that as "Rust has GC" because that might imply that the GC is required. But if your application might benefit from GC, it's certainly available in the language. I might say "GC is optionally available in Rust if desired" to be more accurate.
https://news.ycombinator.com/item?id=46684673 (reply to sibling)
> I think it's fair to expect that it will stabilize in time.
The allocator work is facing a lot of obstacles, unfortunately. I prefer it to be unstable for as long as it needs, though.
> Quite a few have already been implemented and are available as libraries exactly as you describe, today. I wouldn't phrase that as "Rust has GC" because that might imply that the GC is required.
This is exactly my point. Rust does not prevent GC by any means, but Rust also does not encourage GC by any means. The same goes for custom allocators, aside from the highly-unstable Allocator API.
> The allocator work is facing a lot of obstacles, unfortunately.
Got any links? Sounds like interesting reading. Seriously. Kind of thing I come here for. I'd really appreciate it. Or I can ask the AI for some.
> I prefer it to be unstable for as long as it needs, though.
Sure, same. Fully baking things is a process, and it's nice when things are fully baked. So I agree. I think Rust's async could be a bit more ergonomic, though it wasn't too difficult to wrap my head around, and sort of shockingly simple to implement a basic no-std async executor (~10 lines), so maybe I'm coming around. I was pleased to find out that it was simple enough that I could do it, and understand how it worked, as I try to do with all my microcontroller code, and wasn't dependent on a big async runtime like Tokio if I didn't need it's features (or bloat).
https://old.reddit.com/r/rust/comments/1op6g64/whats_the_sta...
This Github thread is also interesting (transitively reached from the above Reddit thread):
https://github.com/rust-lang/rust/issues/32838#issuecomment-...
Thanks, I'll give 'em a read.
No, it isn't. Because that distinction is significant if you are using the language in an environment where those libraries are not available or suitable, such as the Linux project which uses a custom fork of Alloc which provides collections for different allocators.
So, that is not done at the language level but the library level. Unless the compiler is modified for Linux, but even if it is, that's entirely bespoke and unstable. This is not comparable to Zig's design. I'm aware that anything can be done if the right things are implemented; I'm talking about what Rust-the-language currently does for control over allocation. If the answer comes down to "we're doing something custom", then the language is soewhat or largely sidestepped. C-the-language certainly doesn't have panics or exceptions, even though longjmp or a custom runtime could be used.
Rust the language knows nothing about allocations. It’s purely a library concern.
For the time being, I consider alloc and std to be part of the language, and the compiler also has provisions for them if they are used. If the alternative is supposed to be only using core, then the language does not provide for control over allocations the way Zig does. Imposing allocations with no control and not providing allocations at all are both failure modes. With enough effort, anyone could do anything, but programming languages exist to control and enhance certain things under their purview. Rust does not have a comparable facility to control allocations like it controls ownership and borrowing. Just as C-with-tooling being safe isn't the same as C being safe, Rust-with-libraries providing control over allocations isn't the same as Rust providing control over allocations.
> The real answer should have been a new language that has memory safety without all the extra conceptual changes and orthogonal subsystems that Rust brings. The core value of safety did not need the reinvention of everything else with the accompanying complexity and cognitive load.
What would the minimal set of features be, in your opinion?
> For example Zig which instead of introducing a new metaprogramkming language, it uses...... Zig - imagine using the same language instead of inventing a new additional language with all the accompanying complexity and cognitive load and problems.
Zig probably isn't the best comparison since Zig doesn't try to achieve the same level of compile-time memory safety guarantees that Rust aims for. For instance, Zig doesn't try to statically prevent use-after-frees or data races.
That being said, as with everything it's a question of tradeoffs. Zig's metaprogramming approach is certainly interesting, but from what I understand it doesn't offer the same set of features as Rust's approach. For example:
- Zig's generics are more similar to C++ templates in that only instantiated functions are fully checked by the compiler. Rust's generics, on the other hand, are completely checked at the definition site so if the definition type-checks the author knows it will type-check for all possible instantiations. Rust's approach also lends itself to nicer error messages since everything a generic needs is visible up front.
- Zig's comptime isn't quite 1:1 with Rust's macros. comptime is for... well, compile-time computation (e.g., reflection, compile-time branching, or instantiating types). Macros are for manipulating syntax (e.g., code generation or adding inline support for other languages). Each has things the other can't do, though to be fair there is overlap in problems they can be used to solve.
In any case, metaprogramming approaches are (mostly?) independent of memory safety.
> And result and option and move by dedfault - none of these things were needed but they all add up to more complexity and unfamiliarity and cognitive load.
I don't think Result/Option are that complex (if at all) since they're trivially derivable from discriminated unions/sum types/enums.
I'm also not sure how move by default is necessarily "more complexity... and cognitive load"? Maybe as a result of unfamiliarity, perhaps, but that seems more a property of a person than a language, no?
There's also https://docs.rs/proc-macro2/latest/proc_macro2/ which effectively allows for the construction of procedural macros with normal Rust possible. As well as enable the use of proc_macro types in normal rust. It'll make it to stable someday, I hope.
> The real answer should have been a new language that has memory safety without all the extra conceptual changes and orthogonal subsystems that Rust brings.
So what you're saying here is that you don't understand that Rust's rules around memory ownership, aliasing, and mutability are what allow the language to provide deterministic compile time memory safety without runtime cost. If you figure out another way to guarantee memory safety at compile time with zero runtime overhead, you should write a paper and start another language around it!
https://en.wikipedia.org/wiki/Capability_Hardware_Enhanced_R... exists, and is an exciting, laudable effort, I think. But requires hardware support as well as language modifications.
And even if you a runtime solution with no runtime cost, you'd still need to run the code, to find the memory safety bugs. Static analysis is supposed to tell you there is no path that violates memory safety.
> In my experience (20+ years with C/C++, and about 4 years with Rust), Rust is significantly less complex than C++, while being similarly capable
OK, what about if you have an existing code base of hundreds of thousands, or even millions, of lines of C or C++? Still think it's going to be better, and especially cheaper, to "rewrite it in Rust" instead of just porting it to Fil-C or something similar?
Oh I'm all for pragmatism. I think Fil-C is great in that regard. For shoring up legacy code and maybe even for new projects if you're a C/C++ shop and velocity is important and you can eat the performance and memory costs.
I've also written some bindings for C/C++ in Rust and it couldn't have been simpler. bindgen is amazing. So with the experience I have today, I wouldn't hesitate to start rewriting portions of a C/C++ project in Rust where it made sense. And keep the thing building before, during, and after.
I'm currently in process on a rewrite of the RepRap.org stack I helped write 20 years ago in C++ and avr-c++, Python, OpenSCAD, and other languages to idiomatic Rust in order to reduce the number of languages involved, support building a fast wasm user interface, build the interface and firmware in the same language and share types across the boundary, explore GPU compute, enable multithreading of interface and firmware, and explore async firmware architecture. And I'm enjoying it a lot.
Compared to writing it the first time, I am spending zero time fighting build systems, able to reuse much more code from libraries much easier, and have been able to delve deeper into complex issues like advanced numerical representations, ensuring algorithmic determinism under all possible cases, and the implementations of vector math library which is the foundation of much of the code. Even egui, the UI toolkit I've been using, seems simpler and easier to work with than QT or GTK or anything I've worked with save FLTK which was dead simple. I've really enjoyed Rust for all that.
I have the sense, after working with rust, that I am much less likely to get myself into trouble with it than with C++, and much more likely to know where that might happen. Past some point, my big C++ projects got big enough that it got difficult to hold a coherent picture of the whole thing at runtime in your head all at once, to understand it's behavior. That point seems to be farther out, for me, in Rust.
The most challenging thing about the rewrite has been chasing down all the little bits of state hidden away in various C++ functions, and normalizing all that into one of a few architectures which work well with Rust's borrowing rules. But the process of doing so consists entirely of things which are best-practice anyway, and the resulting code is definitely easier to read and understand to my eye.
These are just observations from my position, as a person with a big legacy C++ codebase, currently rewriting in Rust. And occasionally working in other languages, of course. I have lots of projects.
C% agree.
Rust is better than C++.
I don’t know what you mean by SafER but it’s important to remember that Fil-C sacrifices a lot of performance for that safety which detracts the reasons you’d be running that software as otherwise C was a bad language for them. Sometimes this won’t matter but there are places fil-c won’t be able to go that Rust can - embedded and OS kernel come to mind. Other places would be things like browsers or games. Rust gives you the safety without giving up the ability to do performance.
Also, I could be wrong but I believe any assembly linked into Fil-C bypassed the safety guarantees which would be something to keep in mind (not a big deal generally, but a source of hidden implicit unsafe).
I’m apparently comment happy on this OP, but, the typing of it looks funny because it starts the sentence, I’m pretty sure OP was saying safER, as opposed to SAFE (as in totally safe instead of comparatively safer). I have been quite charitable to OP in some sibling comments and will do so here. I think OP is attempting to give Fil-C some credit for being an attempt to increase the overall memory safety of existing code without incurring the complexity of a new language or the complexity of rewriting long running/widely distributed code. It is a decent sentiment and a viable methodology to achieve a laudable goal, but is certainly susceptible to caveats like the performance penalty you mention.
If you can’t understand ownership I’m baffled how you believe you can write well behaved C or C++.
Rust at least embeds this information in the API with checks. C and C++ are doc comments at best.
I’m going to start this comment by specifying that I don’t know what OP was considering complex about Rust and, unfortunately, a large amount of discussion on the topic tends toward strawman-ing by people looking to argue the ‘anti-Rust’ side of said discussions. Additionally, the lack of a contextual and well considered position against some aspect of Rust, as a language, is very common, and at worst the negative take is really just a overall confrontational stance against Rust’s uptick in usage broadly and its community of users, as perceived (and also strawmanned), in a generally negative light. But since borrowing is not explicitly mentioned by GP, I will give a slightly different position than he might, but I think this is an interesting perspective difference to discuss and not a blatant ad hom argument used to ‘fight’ Rust users on the internet.
From my position the complexity incurred by ownership semantics in Rust does not stem from Rust’s ‘formalization’ and semi-reification of a particular view on ownership as a means of program constraint. The complexity of Rust, in relation to ownership, comes with the lengths I would have to go to design systems using other logical means of handling references (particularly plain hardware implemented pointers) to semantic objects: their creation, specification, and their deletion. Additionally, other means of handling resources (particularly memory acquired via allocation): its acquisition, transport through local and distributed processes (from different cores to over the wire), and its deletion or handing back to OS.
Rust adopts ownership semantics (and value semantics to a large degree) to the maximum extent possible and has enmeshed those semantics throughout all levels of abstraction in the language definition (as far as a singular authoritative ‘definition’ can be said to exist as a non-implementation related formalism). At the level of Rust the language, not merely discussions and discourse about the language, ownership semantics are baked in at a specified granularity and that ownership is compositional over the abstraction mechanisms provided. These semantics dictate how everything from a single variable on the stack to large size allocations in a general heap to non-memory ‘resources’, like files, textures, databases, and general processes, are handled in a program. On top of the ownership semantics sit the rest of Rust’s semantics and they are all checked at compile time by a singular oracular subsystem (i.e. the borrow checker).
The complexity really begins to rise, for me, if ai want to attempt to program without engaging with ownership as the methodology or semantics for handling all of the above mentioned ‘resources’. I prefer, and believe, that a broader set of formalisms should be available for ‘handling’ resources, that those formalisms should exist with parameterized granularity, and that the foundational semantics for those mechanisms should come from type systems’ ability to encode capabilities and conditions for particular types in a program. That position is in contrast to the universal and foundational ownership semantic, especially with the individualistic fixed granularity, that Rust chose.
That being said, it is bordering on insanity to attempt to program in such a ‘style’/paradigm/method in Rust. My preferences make Rust’s chosen focus on ownership seem complex at the outset, and attempts to try and impose an alternate formalism in Rust (which would, by necessity, have to try and be some abstraction over Rust’s ownership semantics which hid those semantics and tried to present a different set of semantics to thenprogrammer) take that complexity to even higher levels.
The real problem with trying to frame my position here as complexity is the following: to me Rust and its ownership semantic is complex because I do not like it’s chosen core semantic construct, so when I think about achieving something using Rust I have to deal with additional semantics, semantic objects, and their constraints on my program that I do not think are fit for purpose. But, if I wanted to program in Rust without trying to circumvent, ignore, or disregard it’s choices as a language and just decided to accept (or embrace) it’s semantic choices the complexity I perceive would decrease significantly and immediately.
For me, Rust’s ownership semantics create an impedance mismatch that at the level of language use FEELS like complexity (and acts like complexity in a lot of ways), but is probably more correctly identified as just what it is… an impedance mismatch, nothing more and nothing less. For me, I just chose not to use Rust to avoid that, but for others they get focused on these issues and don’t actually get to the bottom of their issues and just default to calling it complexity during discussion.
All in all, I am probably being entirely to optimistic about the comments about the complexity of Rust and ownership and most commenters are just fighting to fight, but I genuinely believe there is much to discuss and work through in programming language design theory and writing walls of text on HN helps me do that.
Rust's strict ownership model isn't flexible enough sometimes (particularly when dealing with hardware concepts), so I consider it a "mostly good enough" abstraction that is a good default but should be circumvented sometimes.
My friend this is the comment section, surely this could be said in fewer words
Tl;Dr
The TL;DR was exhaustion and medication make me chatty. I didn’t realize how long the comment was until I got back on just now.
I agree that your previous comment could be shorter, maybe half as long, with some effort. For example, this sentence could just be removed without changing the content or tone:
> But I think this is an interesting perspective difference to discuss and not a blatant ad hom argument used to ‘fight’ Rust users on the internet.
(It is already clear that you share your perspective because you think that it's interesting and not a fallacy.)
Then, there are some paragraphs close to the end that seem to repeat ideas.
But IMHO it's a good, nuanced comment that both addresses shortcomings in current discourse and adds ideas to the discussion. This is obviously difficult to do in few words.
A few concepts that come to mind from your comment, but I missed from others:
- Are Rust-style ownership semantics complex or just hard for me? Is it an issue of familiarity only? Is it ergonomics?
- Are they hard in general or just due to the way I like to program? How can I change my style to better fit the model?
- Are they even fit for the software I write? i.e. is it worth it to change how I program to better fit the model?
- What other tools are there to deal with resources?
- What could a programming language do to offer multiple of those? How can we mix GC, region types, linear/uniqueness types, manual management, etc. in a single language?
- A bunch of stuff about discourse in this thread and HN/the internet in general (which is maybe not the point of the comment and could be omitted?)
ICOS (from NASA) and Fil-C together make it possible to make more safe code than vanilla Rust.
https://github.com/NASA-SW-VnV/ikos
I think it might be an interesting experiment to try to duplicate as much of the functionality of IKOS as possible in vanilla Rust using a no_panic-like [0] technique. My guess is that most of the checks are already done by Rust or can be covered by such a technique, albeit perhaps with more hand-holding than for IKOS. The pointer alignment and comparison checks are the ones I'm least certain about since Rust is relatively lax with those.
[0]: https://docs.rs/no-panic/latest/no_panic/
Rust is a much easier language to master than C++.
I’m pretty sure there is not any realistically feasible way to ever prove your statement. But I hope a majority of people can recognize the sheer magnitude of C++ as a language and take a position that it may not be possible to master the whole thing. Rust is ‘smaller’ language using some metrics (most metrics really) than C++ is another thing I would hope most people can accept. So, given that the comparisons between the two wholes being a semi-intractable discussion I would propose the following:
When considering some chosen subset of functionality for some specified use case, how do Rust and C++ compare in the ability to ‘master’. There are wide and varied groups (practically infinite) of features, constructs, techniques, and implementations that achieve targeted use cases in both languages, so when constructing a given subset which language grants the most expressivity and capability in the more ‘tight’ (i.e. masterable) package?
I think that’s a way more interesting discussion to have. Obviously, where the specified use case requires Rust’s definition of memory safety to be implemented 100% of the time (excluding a small-ish percentage of delimited ‘unsafe but identifiable’ sections) the Rust subset will be smaller due to the mandatory abstractions required to put C++ anywhere near complete coverage. So it may make sense to allow the subset to be defined as not only constructs in the base language, but include sealed abstractions (philosophically if not in reality) as potential components in the constructed subsets.
I may have to try and formulate some use cases to pose in a longer something to see if any truly experienced devs can lay out their preferred language’s best candidate subset in response. It would also be fascinating to see what abstractions and metaprogramming would be used to implement the subset candidates and figure out how that could factor into an overall measurement of the ‘masterable-ness’ of the given language (i.e. how impossible a task is it to be able to rely on a subject matter expert to implement any proposed subset for any given use case).
It could be interesting to try to apply Fil-C techniques to Rust. After all, while safe Rust is fully memory safe and has many other forms of bug-resistance that Fil-C lacks, Fil-C is far safer than unsafe Rust. Maybe one could achieve both.
And there are probably a lot of optimizations available by skipping safety checks in safe Rust where the compiler could prove that the check was already done.
> It could be interesting to try to apply Fil-C techniques to Rust.
The bulk of the work Fil-C does is implemented as an LLVM pass [0, 1], so at least in principle it should be possible to get Rust code to compile using Fil-C as a "backend".
[0]: https://fil-c.org/how
[1]: https://fil-c.org/compiler
rust is not complex for C++ experts. Both have the same concepts. For beginners of course rust is harder, but that didn't mean bad for beginners
This is awesome. Would love to see if it catches some of the Qt bugs I found but haven't been resolved yet[1].
[1] https://qt-project.atlassian.net/browse/QTBUG-122658
I only tried to get Qt Base up and running. But Qt Declarative will be next, after Qt Base.
That would be awesome (:
AFAIK Fil-C does not catch all memory safety bugs, for example some use-after-free are just not bugs but work as intended (you still access the original data/allocation). This means that it's not a sanitizer and code that runs fine on Fil-C may show UB when run normally.
> for example some use-after-free are just not bugs but work as intended (you still access the original data/allocation)
That doesn't sound right? For example, from the Fil-C GC docs [0]:
> If you call `free`, the runtime will flag the object as free and all subsequent accesses to the object will trap. Additionally, FUGC will not scan outgoing references from the object (since they cannot be accessed anymore).
[0]: https://fil-c.org/fugc
Any use after free is a bug. There is no way you can use that area without ownership.
eh, I daily-drive a -fsanitize=address -fsanitize=undefined build of Qt and actual memory bugs are almost never a thing - I think the only time I had some were in tooling executables such as qmllint, but not in the framework itself. Most of the bugs by large are more "behaviour" bugs.
I’m impressed that QT runs clean enough under ubsan for daily use.
ubsan definitely has some warnings due to a few "technically UB" patterns used as optimizations in Qt as they are known-working on every target platform under the sun, but yeah, no crashes :)
I'm only using this configuration for the software I develop though (+ libc++ debug mode) as it's painfully slow, but it exercises the Qt codebase in depth.
Here is another bug that led to crush thatt I reported while developing my block editor:
https://qt-project.atlassian.net/browse/QTBUG-124572
Good to know! Which version of Qt are you using?
I regularly update, right now 6.10.1 + a few patches
Depends in which sense you want it to "catch" the bugs. As this readme notes/quotes,
> All memory safety errors are caught as Fil-C panics.
If your problem is a memory-based bug causing a crash, I think this would just... catch the memory-based bug and crash. Like, it'd crash more reliably. On the other hand, if you want to find and debug the problem, that might be a good thing.
Sure, if the memory error is an immediately crashing one like a null per deref, but if is (for example) a memory corruption (e.g. an out of bounds write or a write-after-free) then this would be super helpful in exposing where those are happening at the source.
Half a second's worth is a lifetime of debugging at today's clock rates so the closer to the fault the crash happens the sooner you can fix it.
That’s what “catch” means here. As in, catch it in the act. Tools that make bugs crash more reliably and closer to the source of the problem are extremely valuable.
It would be nice to have one thread about Fil-C without it becoming about Rust.
I thought the point of Fil-C was to be a drop in safe replacement of C. This project existing implies it isn't. What's going on?
Fil-C is as compatible with C as different variants of C are with one another.
The porting effort is usually a less than if you were going from 32-bit to 64-bit for the first time. For some programs, you need zero changes.
In Qt I think the changes were stuff like:
- Use an intrinsic instead of inline assembly for cpuid.
- Change how a tagged pointer works (I.e. a 64-bit value used to store some integer bits and a pointer)
(Source: I’m the Fil-C guy and I watched the Qt porting happen from the sidelines.)
Afaict, there are some patterns that are not supported, like converting pointers to/from integers and doing stuff with them like bitmasks (which is a huge anti-pattern, but some code bases do it)
It isn't entirely drop-in for all programs.
Most large C code bases aren’t really written in C. They’re written in an almost-C that includes certain extensions and undefined behavior. In this case, it uses inline assembly (an extension) and manipulating pointers as integers (undefined behavior).
While I’m always thankful when people give the broad perspective and context in a discussion, which your comment does. The specifics of this particular project’s usage of almost-C is not something I could have quickly figured out, so thanks. For such a large program, an to be as old as Qt is at this point, I find it impressive and slightly amazing that it has in some sense self-limited its divergence from standard C. It would be interesting to see what something like SQLite includes in its almost-C.
The more portable a project is, the less weird stuff it’s likely to do. The almost-C parts become more of a headache the more OSes and compilers you support. This seem pretty tame, and I’d expect SQLite to be similar. I work on some projects that only support a single OS, compiler, and CPU architecture and it’s full of dependencies on things like the OS’s actual address space (few 64-bit archs use all 64 bits).
My first job was on a large-ish software product that ran on several completely incompatible platforms - various Unixes, early Windows, IBM mainframes, etc - and window systems. At first, making all of them happy seemed like annoying busy-work.
But our code was extremely clean and extremely well-factored, because it had to be. And after porting our product to the first two or three new platforms, the later ones were much much easier to do. Lesson learned.
Since (say) the 1990s, it feels like "the world" has slowly converged to pretty much (a) the browser; (b) desktop computers - Windows, Linux, Mac; (c) phones/tablets; (d) everything else (mainframes, embedded, industrial, what have you - stuff that most people will never deal with). And portability across different platforms is no longer all that important. Which is fine, but I do miss how the need for portability forced us to work with discipline and be relentless on quality.
QtBase is C++ first of all.
Massive projects like Qt also push compilers to their limits and use various compiler-specific and platform-specific techniques which might appear as bugs to Fil-C.
It's not done yet.
Sure fooled me. I follow his Twitter account and there isn't much he hasn't got building with it at this point. UX comes later. Amazing it's the random work of one person
The author wrote WebKit’s allocator and worked on JavaScriptCore for over a decade. I really enjoyed his posts on the WebKit blog over the years like this one on the concurrent garbage collector (2017) https://webkit.org/blog/7122/introducing-riptide-webkits-ret...
I don’t think so much is fil-c itself, but from the looks of the diff it’s a new platform essentially. That can require porting existing software generally which you can read from the posted diff
Very cool, thanks! I will try this with Mudlet.
Interesting to see Fil‑C used with a large framework like Qt. The fact that it compiles with minimal changes says a lot about the compatibility layer and the InvisiCaps approach.