That said: almost all your points apply to C++.
There's nothing simple about C++.
it can be incredibly complex and slow (though faster than Rust, but that's not much of an achievement).
The rules around what constitutes valid (non-UB) code is anything but simple.
It isn't uncommon for the syntax to look arcane (IMO much more so than Rust, although obviously YMMV).
The standard library is extremely shallow, making developers have to resort to third party libraries for trivial things, except there's no package manager or standard build system, so good luck with that.
And yet, here we are, with C++ dominating the market. So I don't think any of those reasons are dealbreakers at all.
I believe Rust has the potential for market adoption as a serious C++ competitor. It still has a long road to get there, but C++ didn't reach this point in a day either. We see more and more adoption in big companies of Rust. Microsoft recently expressed interest in using rust in critical components to reduce security bugs for instance. More and more companies are experimenting with Rust, and some are even using it in production.
Besides, there's another victory to Rust: more and more languages are looking into integrating borrow semantics in their own language, like swift[0]. This is proof that Rust's core ideas work. If those trickle down to other languages, the "complexity" of borrows will probably become just one of the other things you just have to get used to to learn programming.
[0]: https://github.com/apple/swift/blob/master/docs/OwnershipMan...
And nobody here said C++ was a good language. Actually, is it even considered a systems programming language?
Yes, absolutely. It's really not even under any sort of doubt at all..?
I keep seeing people saying this when discussing Rust in relation to C++. It's not untrue, but I would counter by saying that people who were writing C++ without having a full understanding of ownership and lifetime semantics were writing buggy code. Rust just makes understanding those semantics required to get your code to compile.
I took CS 101 ~ 301 in college, and have worked briefly in C++ here and there during my career. I like to think I have a working understanding of pointers and references.
Despite that, whenever I go into a C++ codebase (recently, the Chromium codebase) I get _completely lost_ trying to figure out what the owner and lifetime of an object is, whether it's appropriate to pass by value, reference, or clone. I have absolutely no clue the impact of these decisions aside from a vague sense that pass-by-reference is more memory efficient and cloning is safest (When do we care? Is it something you can just pick one and forget about until performance matters?).
How do you start to pick up an intuition for manually working with memory this way? Is there a set of references that can take you past the beginner level? I find the C++ reference documentation completely opaque, and most search hits tend to be too domain specific to be useful.
Is this something that Rust would help with? Is their model easier to understand when each case is correct? Is it something that comes with language exposure? Or tooling?
It took me a little bit to get used to, but learning this definitely helped me learn to think about ownership and lifetimes better. I highly recommend you try learning some Rust.
While I did preserve to make it work, not everyone is willing to keep trying.
Default assumption: all data lives in a value type, or an STL container. The destructor/copy constructors will automatically deal with lifetimes for you.
Fallback #1: the remainder lives in a std::unique_ptr, or perhaps std::shared_ptr. The destructor/copy constructors will deal with lifetimes for you.
Fallback #2: Take a moment to reflect upon the mistakes that you've made that have led you here. This is your fault. Write a class with correct constructor, destructor, copy constructor, move constructor, and move/copy assignment operators. This is roughly the equivalent of resorting to unsafe in rust in a place that makes calls to other rust code. (as opposed to a syscall or a c function call or somewhere else that it's obviously required) If you're here, it's because you fucked up.
Since c++11 had become available, I've eliminated all use of new/delete from new code that I write. I've slowly refactored old code (by myself and others) to do the same. The only significant attention I've paid to lifetimes is when I'm using a c library or am writing c# where half my objects are IDisposable. (yes, really, I do more manual memory management in a garbage collected language)
The problem with c++ isn't that you have to worry about lifetimes, it's that you used to have to worry about object lifetimes and many codebases were written during that time. Sure, we could rewrite the world in rust and all of that would go away, or we could rewrite the world in modern c++ and it would also go away.
It is still in the early stages and work is being done to make it a common feature across major compiler, while improving their capabilities.
Anywhere you have any kind of containment hiearchy, Rust starts becoming more invasive than other languages. In languages like Go, you can have complex graphs of objects referring to each other, and you can — often arguably safely — work with these structures without thinking about who owns what.
As an example of something that got me stuck, I recently had some code that populated a map of mutable buffers:
struct Builder {
buffers: BTreeMap<Term, RefCell<PostingBuffer>>,
}
At the end of the building, it needs to flush the buffers, in key order, to a file and then empty the map so it can be reused. Turns out this is trickier than expected because the map owns its contents, so you can't take ownership of the RefCell that wraps the buffers: for (term, buf) in &self.buffers {
// into_inner consumes buf, so it moves; fails with "Cannot move out of borrowed context"
let data = buf.into_inner().get_data();
w.write(term, data)?;
}
self.buffers = BTreeMap::new();
In the end, the trick was to replace the map for the iteration: for (term, v) in std::mem::replace(&mut self.buffers, BTreeMap::new()) {
let data = v.into_inner().get_data();
w.write(term, data)?;
}
Initially I used a HashMap to optimize for build speed, then sorted its keys at the end. This was also a challenge, because maps apparently have no way of getting a copy of the keys by value. (There might be an easier way, but again, this just illustrates the learning curve for new developers.) In the end, I chose BTreeMap so the keys are already sorted.This is all probably entirely obvious to Rust experts, but not so to new developers. Swapping out the entire map with std::mem::replace() would never have occurred to me. Even if you understand borrowing in principle, you have think about a container means in terms of borrowing, and how stuff will move in and out of a container. And you have to design the way you interact with them accordingly.
In principle, I think this awareness is a good thing, and that the resultant code will be smarter and more optimal as a result, but at the same time, it doesn't make for such an ergonomic developer experience; so far it's been a drain on my productivity more than a gain. I like to say that Rust "scales down" towards the low levels, but does less well "scaling up"; you can't write high-level code without also thinking about the low levels, when even things like the size of your data type is always in your face.
My solution above may not even be the most optimal, idiomatic way of doing things. I'm sure someone will come along and point out that there's a trick involving using something other than RefCell or whatever that simplifies everything.
Not necessarily a criticism of Rust, just a data point in terms of complexity and learning curve.
I think that if you wrote
for (term, buff) in self.buffers {
it should have Just Worked. By iterating over a reference to self.buffers, you iterate over a reference to the contents, which you can't move out of. But iterating over it by value, it should give it to you by value, and you'd be fine. That said, I have not tried it, so there might be some context I'm missing. One nice thing about a strict compiler is that I have to think about the details less, and use the error messages to help me fix any problems I find. But that makes it harder to know how it goes without the compiler...The reason I can't just iterate by value is that I'd be consuming data that is already held by the map. The buffer is defined thus:
use bitstream_io::{BigEndian, BitWriter};
struct PostingBuffer<W: io::Write> {
bw: BitWriter<W, BigEndian>,
// ...
}
impl PostingBuffer {
pub fn get_data(self) -> W {
self.bw.into_writer()
}
}
bitstream_io::BitWriter's [1] into_writer consumes itself: pub fn into_writer(self) -> W {
self.writer
}
[1] https://docs.rs/bitstream-io/0.8.2/bitstream_io/write/struct...This is exactly why I couched my last paragraph the way I did, heh.
I'd rather have a thriving and single (looking at you Python) ecosystem of third-party packages, than a standards-driven pile-on of standard libraries, with all the footguns and deprecation that tends to come with it.
It doesn't mean that the language is complex.
> The borrower semantics is anything but simple.
The borrow checker doesn't change the meaning of the program in any way. You can perfectly understand Rust code without any knowledge of the borrow checker (there is a working Rust compiler that doesn't even have a borrow checker), so it cannot make the semantics of the language more complicated.
> It isn't uncommon for the syntax to look arcane.
Again, I did call out the syntax has being moderately complicated. The great thing about syntax is that it's superficial. Syntax has to be pretty bad to continue being an issue beyond the initial stages of learning a language, and I don't think Rust's syntax is close to being that bad, it just has some unfamiliar elements to a lot of people.
> The standard library is shallow often making developers have to resort to third party libraries for trivial things.
This sits on the "Rust is simple" side.
> Rust improved upon languages like Cyclone but it's a far cry from being the next C/C++ when it comes to potential market adoption.
In terms of actual market adoption I'd agree. But you said potential market adoption, so I don't.
This means that it's possible to write a compiler that correctly compiles all correct rust programs without having a lifetime system at all. It also means that having the lifetime system in makes writing programs more complicated, but it makes reading them, and understanding what a program does, easier.
The type system is still relatively simple compared the type systems in Java/C#/C++/etc. Let's take Java's as an example because I think people regard it as being simple:
Building blocks of the type system: Rust: Types, Traits Java: Primitive types, classes, interfaces, (value types yet?)
Relationships between those building blocks: Rust: Implements (Type <=> Trait) Java: Extends (Class => Class | Interface => Interface), Implements (Class => Interface), Auto-boxes (Primitive/value type => Class)
Generics: Works mostly the same in both languages. In Java you have to deal with the side-effects of type-erasure from time to time, and there's also the `? extends Y` syntax, for which Rust has no counterpart. In Rust you can have a generic parameter which is a lifetime, for which Java has no counterpart. Again though, these lifetimes are only used by the borrow checker, unlike normal type parameters, they do not affect the generated code.
Java appears simple because it is built on OOP concepts that are very familiar to most people. However, those OOP concepts contain a huge amount of complexity (inheritance trees, constructors, abstract classes, virtual/final methods, monitors, etc) and Rust simply gets rid of all of those concepts entirely. That's why I think it's simple.
I was with you till this point. Rust is ugly, complex, but far batter that C++ in this regards.
* Functions instead of 666 constructors with obscure rules of which you should implement in pairs
* Hygienic macros instead of that ugly unsafe mess in C and C++ called macro and templates
* Move by default and Copy by calling a copy() function
* Everything is so explicit
But the best parts of Rust are its move semantics, enum datatypes and traits, everything that makes programs a joy to express.
>but it's a far cry from being the next C/C++ when it comes to potential market adoption.
Because C++ is very similarly complex/arcane etc.
There's nothing simple about Rust.
The compiler is complex and slow.
The borrower semantics is anything but simple.
It isn't uncommon for the syntax to look arcane.
The standard library is shallow often making developers have to resort to third party libraries for trivial things.
Rust improved upon languages like Cyclone but it's a far cry from being the next C/C++ when it comes to potential market adoption.