I'm not sure you can argue that Rust is far more complex if you actually understand C++ sufficiently to write correct and performant programs.
But the really great thing about Rust is that you don't actually have to understand the language to write correct and performant programs. The compiler will stop you in your tracks every time you screw up. Also there's way less performance foot-guns by default. Everything is moved instead of deep-copied by default, and you can take random pointers into things without having to worry about it.
But it's true, complicated things are complicated to express, which seems... natural to me? I dunno what people expect. But simple things are actually really simple.
For instance, here's an early version of a library I'm working on for decoding gifs: https://github.com/Gankro/gif-rs/blob/master/src/lib.rs
There isn't a single lifetime in sight. It's also really nice and readable (being able to ergonomically talk about bit and byte patterns was a big boon). Granted the library is fairly naive (it's a pull parser, but it later became clear I wanted a push parser for Servo). But I think it's a great argument for simple things are simple.
Rust gets crazy when you demand on writing extremely generic code like Hyper, because Rust leans heavily on static verification. But generic code is just always going to be cray-cray (See: Haskell lenses, C++ Boost, Scala collections).
I'd be surprised to hear that C++ is less complex than Rust (going by number of features or concepts or some reasonable complexity measurement).
For example, assembly language is very simple. It's basically just arithmetic, comparisons, i/o, and gotos, right? But building systems using assembly results in extremely complex code because it lacks the tools to manage complexity well. You end up with a rats nest of spaghetti code often, for example, unless you use structured programming macros. C++ seems "simple" but it ends up being essentially a "write-your-own-language" language with the way you can use templates, macros, and operator overrides. There's no such thing as one C++ language, there are actually hundreds of dialects that are used by different groups of people for different purposes, almost all of them effectively mutually inoperable with one another. This is a complexity nightmare.
Rust is a systems language, like C/C++, but designed from the ground up and based on modern techniques. For example, there are ways to use C++ which are particularly advantageous from a memory management aspect, such as RAII, but enforcing such uses comes down to culture/policy. Rust instead is based on different principles, specifically the idea of "borrowing", to handle memory management instead of relying on either extreme of "do what you want" or "the GC will clean up after you". There are a ton of other concepts which are baked into the Rust design which result in the ability to create programs that are easier to maintain, more likely to be correct, and more likely to be safe/secure, without sacrificing performance relative to C++ running on bare metal.
I was taking with someone just today about getting discriminated unions (similar to Rust's enums) into C++17 and there are just _so many_ details.
However I will say that Rust makes it a lot harder to accidentally leak resources (it's trivial adversarially -- but I don't consider that an interesting programming environment YMMV).
In e.g. Java you need to remember to `close` a File -- in Rust it will close itself as soon as you stop using it unless you do some wild things. We can't formally guarantee anything better than the Java scenario; it's equivalent to whoever you pass a file to having to call `close` on it.
However the defaults are reversed: In Java you don't close a File by default, and have to explicitly do something to close it. In Rust you close a File by default, and have to explicitly do something to not close it (mem::forget or Rc cycle).
This is a pretty general theme for the wins Rust provides: it picks the best default so simple things are simple, but gives you all the tools to bypass that default. By default you want to close files. By default you want to free memory. However you can leak these resources when it makes sense.
The most interesting case for me is our HashMap: we default to SipHash seeded with some OS entropy to prevent algorithmic complexity attacks. This is an explicit choice to protect our users against a relatively obscure problem. Honestly, it's probably not what most users of HashMap need. Determinism and speed is probably nicer. And yet the cost of not having that safety guard when you need it is pretty extreme.
However if you know what you're doing you can just grab a community crate with a different hasher and plug it into our HashMap. Easy as a single type annotation: https://github.com/shepmaster/twox-hash/blob/master/README.m...
Rust doesn't break RAII in the usual sense. The leak issue in Rust is not something that can be cleanly talked of in the context of other languages.
In all languages with RAII till now, it's possible to "leak" memory by sending things to a permablocked thread, or stuffing them in a global hashmap, or whatever. This is something that no compiler can prevent, and it may even be desired at times.
Rust has always allowed this sort of "leaking". So have all the other languages out there.
However, Rust has the concept of scoped/borrowed data -- data with a lifetime which cannot escape that scope. Stuff that isn't scoped is said to be `'static`, i.e. it's lifetime can be the lifetime of the whole program (we can keep moving it out of functions and throwing it between threads). Scoped data can't be shoved into a global hashmap or sent to a random blocked thread, because it's not allowed to escape its scope.
Except, it turns out that using stuff like Rc cycles, it's possible to leak such data too. This affects RAII guards which use lifetimes to bind themselves to a scope to say "run destructors when this particular scope ends" (this is different from "run destructors when this object is no longer accessible", because with regular objects they can be hoisted into a larger scope by moving).
Note that the entire issue is about _scoped_ data (data with a lifetime, containing borrowed references), which is not a concept that exists in other languages.
So Rust still provides de-facto leak safety via RAII, it's just that it is possible in safe Rust to leak stuff, but you have to be explicit about it (global hashmap, mem::forget, etc). This is unlike languages where you must `delete` the object explicitly to avoid leaks.
- untyped
- (dynamically | statically) typed
- weekly...strongly typed
Currently, we only have two terms for a language's memory story: Managed, Unmanaged. Maybe it's time to come up with some more jargon for languages based on their memory story:
- unmanaged
- (dynamically (GC) | statically) managed
- weekly...strongly managed
?
Where "strongly managed" is reserved for some future (if at all possible) language that can guarantee no leaks whatsoever (also it'll statically ensure halting)?
Resulting in most mainstream languages being [dynamically | weekly] managed languages, and Rust being a [statically | weekly] managed language?
I dunno.. maybe that will help with these types of discussions..
FWIW it was possible to extend Rust's types with a ?Leak marker that can protect against scoped data being leaked, but that solution was not chosen.
I thought it was the exactly correct response to a comment that says Rust's main feature is that it doesn't let you leak memory. =/
Yes, the RAII part of ownership isn't the main feature. No, the leakocalypse is not as simple as "Rust lets you leak things"