Looking at the rust rfc for the lightweight clones feature [1]. It took me a while to sort of understand it. Once I did I was excited for the feature but after awhile I was once again struck by the observation that rust is a very complex language to learn.
To me as someone who's not learned either it looks like all the concepts and features of 'true' modern C++ (as opposed to C + a few extra features) spliced with the features and concepts of Haskell.
Yet people seem to like making useful things in it so it must have gotten something right. So I'll probably get around to attempting to really use it again.
[1]: https://github.com/joshtriplett/rfcs/blob/use/text/3680-use....
In my experience, C++ is a much more complicated language. The 8 ways to initialize something, the 5 types of values (xvalues etc.), inconsistent formatting conventions, inconsistent naming conventions, the rule of 5, exceptions, always remembering to check `this != other` when doing a move assignment operator, perfect forwarding, SFINAE, workarounds for not having a great equivalent to traits, etc. . Part of knowing the language is also knowing the conventions on top that are necessary in order to write it more safely and faster (if your move constructor is not noexcept it'll cause copies to occur when growing a vector of that object), and learning the many non-ideal competing ways that people do things, like error handling.
Why check this != other? I've seen this once before in a codebase and concluded it was unnecessary.
Asking as someone whose life became much easier after opting not do anything of the above and just write C in C++ ;-)
Consider a move assign to a vector<int> from src to this. The first step is for this to free its resources, the second step is to assign src resources to this, the third step is to set src resources to null.
If src and this are equal and you don't check for it, then you end up destroying src/this resources and the end result would be an empty vector (since the last step is to clear everything out).
The expected behavior is a no-op.
Right you said move but I was thinking of swap for some reason.
I probably still wouldn't care, unless it's clear that moving to self is even required. Trying to break everything in a thousand pieces and generalizing and perfecting them individually is a lot of busywork.
Thanks for organizing for me my thoughts on why even a restricted modern subset of C++ is complicated.
I have been out of it for a while and dont miss it much, but I thought that the rule of zero, not five, was the modern goal.
I mean yes, C++ managed to hit a uncanny overlap of being very overenginered in some aspects which to make it worse haven't focused at all on non-senior/export dev UX while also being underenginered in others aspects :/
They are trying to fix it in recent ~10 years, but they are also adding new clever expert features at the same time, so I kinda see it as a lost cause.
Rust isn't doesn't have "that" much less complexity if you take all the stable (or worse unstable experimental) things it has.
What makes Rust so nice is nearly always(1) if you don't use a feature you don't have to know about it (not so in C++) and if you stumble about a feature you don't know it's not just normally syntax visible but if you use it wrongly it won't segfault or even have a RCE due to out of bounds writes or similar.
So for C++ you have to learn a lot of the stuff upfront, but are not forced to by the compiler, and getting it wrong can have catastrophic consequences.
But for Rust you can learn it (mostly) bit by bit.
It's not perfect, when you go unsafe you are touching on a lot of "modern compiler" complexity which now also need to map to rust guarantees (but also have better save guards/visibility in Rust).
And async doesn't work so well with the bit by bit learning.
But in my personal experience if you don't have a team only consisting of senior C++ devs I would stay far away from C++. On the other hands with the right guidelines/limitations using rust with junior engineers is just fine (the guidelines matter to not get hangup on the wrong things or over-complicate things).
Note that the community has somewhat soured on that particular proposal and it's probably not going to be enacted in its current form, precisely because it's so complicated. (https://rust-lang.github.io/rust-project-goals/2025h2/ergono...)
Complexity is not necessarily an automatic dealbreaker for a Rust language proposal—lots of Rust features are complicated because they solve problems that don't admit simple solutions—but the purpose of this particular feature is to make it easier to write code that uses reference-counted pointers and reduce how much stuff the programmer has to keep track of. It doesn't let you do anything you can't already do, it just makes it less clunky. So if people can't easily understand what it does, then that's bad.
As a side note, this might just be a me thing, but I distinguish between "complex" and "complicated". Certain things have an inherently complexity. Stripped to their essence, they're still going to have a lot of moving parts. It's just their nature. However, you can also add complication on top of something that makes it more complex than it needs to be to perform its function. Think "complication" in the watchmaker's sense. It might be neat to add a dial that shows the number of weeks since your last dentist appointment, but you don't really need that to have a functional wristwatch, and its presence makes the whole thing a lot more finicky and fragile than it would otherwise be.
Complexity is fine. Sometimes we're working on complex problems without simple, straightforward, correct solutions. I do try to avoid complications, though.
Yeah, I was slightly sloppy about that, but also the distinction doesn't entirely help at this level because people get into vicious fights about whether a particular bit of complexity is really "necessary". E.g., lots of people argue that async Rust is unnecessary complication, either because you can just write all the state machines by hand (which is terrible for expressiveness, but do you really need expressiveness?), or because you can just spawn an OS thread for every task (which is terrible for performance, but do you really need performance?). Whereas the pro-async perspective is, yes, it's very complex, but nothing simpler would have done everything that it needs to do.
Oh, I hear ya. That makes perfect sense, and you're so right: my idea of a complex solution might be someone else's complication, and vice versa. I didn't mean to disagree with you, and surely not to correct you, because not everyone agrees on the distinction I make between the ideas.
I meant that more in the spirit of "oh, while we're talking about this, here are my thoughts on a tangentially related idea".
> Complexity is fine. Sometimes we're working on complex problems without simple, straightforward, correct solutions.
Often the best way to proceed is to just solve a simpler problem :)
> Simple is better than complex. > > Complex is better than complicated.
I wonder if the block-level `clone(var_a, var_b)` was considered. It’s a bit verbose but very explicit and much better than the current situation.
Do you mean something like this? https://github.com/rust-lang/rfcs/issues/2407
I find that Rust is a language that feels insurmountable when you examine it from a distance but once you get your hands dirty it feels natural very quickly.
Case in point, my first read on lifetimes just left me confused. So I used Rc<> everywhere and made a perfectly functional program. Picking up lifetimes after mastering the basics made it a lot easier. Most people won't even need to care about lightweight clones.
> Case in point, my first read on lifetimes just left me confused. So I used Rc<> everywhere and made a perfectly functional program.
Curious, did you run into lifetime issues or just started wrapping everything in Rc<> after reading about lifetimes? Wrapping everything in Rc<> isn't even a bad thing, that's what you have to when you do WASM in a browser.
I still find it confusing sometimes because you're not setting lifetime, you're just giving it a name so that the compiler can reason about it.
Like saying something 'static doesn't make it static, it supposed to mean "live until program closes", but you can totally create things with 'static and free them before program exits.
I think lifetimes are best thought of as constraints.
Like `x: Foo<'a>` means "x is constrained by lifetime 'a" as in "x is only guaranteed to be soundly usable if 'a is 'alive'".
With the implicit context of "you are only allowed to use things which are guaranteed to be soundly usable".
And that "moving" is distinct from lifetimes (i.e. if you move x out of scope you just don't have it anymore, while a 'a constraint on x limits where you _can_ "have" it).
Then `'static` basically means "no constraint"/"unconstrained".
So a `x: X + 'static` mean x has no constraints, as long as you have it you can use it.
Where `x: X+'a` (or `X<'a>`) would mean you can _at most_ have x until 'a stops being "alive". (This doesn't mean the data behind X doesn't life longer, just that in that specific place you can't "have" it at most while 'a is alive).
So then if we look at `&'static A` we have a type which 1) is always copied instead of moved, so you always have it while you can have it and 2) you can always have it. As consequence it must life for the whole program execution. Not because `'static` says "until end of program" but because a unconstrained &reference can only be sound if it exist until the end of the program!!
It's been so long I can barely remember now but I'm pretty sure it was the lifetime propagation that got me... once one struct had it, every struct using that struct needed one, then there was more than one lifetime and combining them (and establishing the rules of how the lifetimes related to each other) bent my brain out of shape. Rc<> let me sidestep all of that.
Yeah, lifetime in structs are PITA. However, IMO using them is "the right choice" only in certain situations where performance hit from not using it is too much. I get why serde uses it, but notice who those don't get exposed to end user that much.
Yeah, once you're putting lifetimes in structs you're liable to run into trouble. In general you only want to do that for structs which are not expected to have long or complicated lifetimes (i.e. if you're expecting it might be allocated on the heap, it's likely too complicated to be workable)
Any time I need to explicitly give a lifetime in a struct I use an Rc or some other container. I’ve been using rust casually for years and every time I try to appease the compiler with lifetimes I realize it’s not worth my time. If someone has a very ELI5 resource that will make me understand when and why to do this I’d appreciate it.
IMO this is something that should just be handled by extra runtime code or a magically smarter compiler. Lifetime management feels like something that matters in a microcontroller or hyper-optimized setting, but never when I’m executing code on Apple Silicon for a random application. And yet the language makes simple GC ergonomically painful. I love the language but don’t really need all of that performance. I would gladly take a 1% hit to increment reference counts.
You're on Apple Silicon, which is the preferred platform for Swift. That does exactly what you're asking for wrt. ARC, and is memory safe.
We have production systems, with ~10k Rust without lifetimes. Sometimes you can get away not using it.
Pretty good observation. I think the getting to know Rust documentation should just start with this. Use forget about lifetimes and just use Rc<> when needed.
It is much more complex than C, sure. But it's so much simpler than C++ and you won't find yourself digging through the reference, or standard, or whatever to understand the code you are writing. It's very close to the sweet spot of "not complex enough to make you bald, not simple enough to make your code complex" while also making correctness the default.
Aside from the complexity not being possible to misuse (which is trivial in C++), I still find it easier. Moreover, even if you didn’t know about this if you use clippy it’ll suggest construct replacements that are more idiomatic. So even if you didn’t know about lightweight clones, you’ll get suggestions to leverage them once they’re available for your codebase (and you can apply the vast majority trivially by asking clippy to do it automatically via --fix). This is distinctly not a superpower c++ has and why the complexity of c++ keeps piling up with either no one using the new features or using them in unnecessarily complicated ways.
> This is distinctly not a superpower c++ has and why the complexity
I guess you don't have that much experience with actual C++ development? Because there's a plethora of static analysis tools and any serious IDE come with refactoring tools on top of that, both assistive and automated, that will suggest fixes as you type. Rust didn't invent anything with clippy, however good the tool might be...
I worked for Coverity and people paid us ludicrous amounts of money to get the kinds of suggestions that rustc and clippy give everyone for free. I'm a huge fan.
> Looking at the rust rfc for the lightweight clones feature
while the Use trait (a better name is proposed [1]) is useful, but I don't see how the .use syntax adds any value over .clone()? If the compiler is able to perform those optimizations, it can also lint and ask the user to remove the unnecessary .clone()/change x.clone() to &x. IMO this is better than the compiler doing black magic.
[1]: https://smallcultfollowing.com/babysteps/blog/2025/10/07/the...
As someone who’s dipped their toes in it, I’d say Rust is complicated in theory but not as complicated in practice. I said on a different topic that LLMs help a bit with suggestions and you can start with suboptimal code, improving it as you get more confident.
Obviously I’m not building anything production ready in it but it’s been useful for a few apps that I’d previously been using Python for.
It is definitely a complex language though I would argue probably much less so, on the whole, than C++.
there is currently a lot of ongoing discussions about "easier light weight clones" not just in the context of the RFC but in general.
And from all the discussions I have seen this RFC is one of the less promising ones as it mixes up the concept of "implicitly doing an operation before moving something into a closure scope" and "light weight clones" in a confusing ambiguous way.
So I don't expect it to be accepted/implemented this way, but I expect something similar to happen.
Like for "light weight clones" use is a pretty bad name and new syntax isn't needed, if we start shortening a `.clone()` to `.use` because it saves 4 letters then we are doing something wrong. Similar if argue for it for niche optimization reasons instead of improving generic optimizations to have the same outcome we are doing something wrong IMHO.
And for the the scoping/closure aspect (i.e. a rust equivalent of C++ closures `[]` parts (e.g. [&x](){...}) then it also seems a bad solution. First it's a operation which relates to the closure scope not the call inside of it, so attaching it to the call inside of it isn't a grate idea. Especially given that you might have multiple places you pass a clone of x in and this leading to a lot of ambiguity not highlighted with any examples in the RFC. Secondly for this concept it isn't limited to "cheap clones" sometimes you have the same pattern for not-so-cheap clones (through it mainly matters for cheap clones). And lastly if we really add ways to define captures, why not allow defining captures.
Now sure if you have a good solution for more compact way to handle "not-copy but still cheap" sharing (cloning of handles/smart pointers) of values there it maybe could make sense to also allow it to happen implicitly outside of closure capture scope instead of the invocation scope. But I would argue it's an extension of an not yet existing handle/smart pointer ergonomic improvement and should be done after that improvement.
(yes, I'm aware they use `use` because it's already a keyword, but making a non-zero cost copy of a handle/smart pointer isn't exactly "use it" but more like "share it" :/)
Fwiw C++ has the exact same problem with moving/referencing into closures, see "Lambda capture" in https://en.cppreference.com/w/cpp/language/lambda.html.
In my opinion, it's not just about the complexity of C++ vs Rust. It's how often the complexity (or anything else) can unpleasantly surprise you, and how serious the consequences of that surprise are.
The problem with C++'s complexity is that you have to remember all of it yourself and if you forget some of it... boom undefined behaviour and elusive runtime bugs!
Rust is definitely on the same order of magnitude of complexity, but you don't have to remember it all. Usually if you forget some complex rule and make a mistake, the compiler will tell you.
That's not true for unsafe Rust, but you rarely need unsafe Rust. Apart from FFI I have yet to use it at all and I've been writing Rust for years.
Async Rust is probably the closest it gets to C++'s "you didn't explicitly co_return in the context of a promise with no return_void? ok carry on but I'm going to crash sometimes! maybe just in release mode, on another OS".
It seems like Rust is being thorough. This is the way.
Rust is more complex than C, yes, but as someone who has used both professionally, it is not even close to being as complex as C++. In fact it is closer in complexity to C than to C++.
Perhaps you can help guide a C expert but C++ avoider (and super-avoider of Rust, so far): If C is 1 in complexity, where does C++ and Rust fall. By 'complexity' here I mean: the ability to keep the Entire Language in your head at the same time. C, although it does have complex corners, is -- mostly -- not overly complicated. (As you can probably tell, I prefer assembly, because it is the least complicated. You can build your own abstractions up, which is the proper way to use assembly). Thank you for any insight; I'm not wedded to my views when shown a Better Way.
If C is 1 and C++ is 100 I would say Rust is like 25.
If your favorite language is assembly and C is too high-level for you then you are probably going to dislike Rust (and Java, Python, and every other modern language).
I don't think Rust is very complex. What you're doing is very complex. It's just that doing it in C/C++ lulls you into a false sense of security.
C++ is incredibly complicated. I mean there's a 278 page book just on initialization [1].
I have seen all sorts of bad multithreaded code that compilers have let someone write. It would've been much harder in Rust but Rust would've forced you to be correct. As an example, I've seen a pool of locks for message delivery where the locks are locked on one thread and unlocked on another. This would get deadlocked every now and again so every half second or so a separate process would just release all the locks.
A very interesting post about this issue.
https://smallcultfollowing.com/babysteps/blog/2025/10/07/the...
> Yet people seem to like making useful things in it so it must have gotten something right.
I'm not commenting on Rust, seriously! But I couldn't help to notice that this sentence is a non sequitur. Something right has been developed in PHP and Visual Basic, even in Javascript and Go; still, those developers who freely choose to use those abominations, they do deserve to be pointed at and made fun of.
Go is a great language.