I've also encountered severe flaws in libraries written by experienced programmers working for companies like Google and Red Hat that the Rust compiler caught during translation to Rust.
That's not counting the immense amount of tooling and features that make managing large projects a breeze in Rust. Managing a 100K Rust project with a large team is easier than managing a 100K C++ project with a large team. You can't guarantee that no one will ever make mistakes.
I find segmentation faults to be common place in C/C++ projects from experienced developers, and it's quite difficult to debug them at times.
Avoiding security related issues based on a whole class of problems prevented in Rust by design is a huge benefit. It's better for compiler to prevent that, than to rely on levels of experience, while anyone can make a mistake in reality.
That's not saying that borrow checker is worthless but it does add overhead and frankly in a lot of scenarios that's just not necessary, even with all that fancy compile time checking you still need to do testing and there are many applications where "working for the stuff tested" is enough. Obviously when you're writing a browser that people will try to exploit and you need to execute third party code and load content from entrusted sources things will be different.
So I get where OP is coming from, Rust safety is oversold IMO - it's nice but it's not the only thing that's interesting about Rust - just the fact that it has modules, an official build system and a package manager that's used by everyone is enough for me to consider it over C++. It's lacking IDE like tooling and meta-programming capabilities (in stable at least)
It's important to emphasize out that this is compile time overhead, not run-time overhead.
For example, consider a simple thread-local counter. In C this would be e.g. an object in the tdata section, accessible via thread-local addressing. But in Rust, you have to use something like RefCell, which requires runtime tracking of mutable borrows. So Rust's borrow checker inflicts runtime overhead in this case.
Now, to be fair, this is only one example and I'm sure you could come up with a better one. But this isn't especially controversial. Rust's entire standard library is living proof that you need unsafe to do some things efficiently. The value proposition is that such things can be bundled up behind an abstraction barrier.
In sum, I think "the borrow checker forces runtime overhead" is an incomplete picture. I think a better picture is, "if you completely ban explicit use of unsafe from your code, then you may miss out on some optimization opportunities." i.e., When the borrow checker fails you, you're given a choice: 1) accept safety and the possible performance loss or 2) accept the onus of proving safety and do the fastest thing possible.
I think the fact that those choices are available is an excellent thing. I choose (1) all the time because not every line of code is relevant to performance. But sometimes I get to choose (2), and I'm thankful that such a choice is always very explicit.
For good measure, here's the unsafe version without atomics: https://is.gd/V1K6nD
Edit: Zero overhead as compared to C, that is. Cell has a slight overhead in Rust because it prevents certain optimizations that aren't the default in C.
(It's a hack that plays on the GP's specific example of a counter. It doesn't generalize arbitrarily to removing use of RefCell for thread locals in safe code.)
I've been programming in Rust and C++ codebases for a while now. In general I find that Rust lets you dance close to the line of unsafety without worrying, resorting to Arc/RefCell only when you have to. In contrast, most C++ codebases I have worked with make use of shared_ptr (or its equivalent) quite often, because the language doesn't give you the tools to thread scoped borrows safely through a chain of functions and have it stay safe in the face of future code changes.
Rust is a great language, but people have to stop "marketing" it by saying things like: no matter how great you are with C, you're going to fuck up at some point, so that's why you should be using Rust. Like Go or whatever other language, the main grab needs to be a positive: you know what you're doing, and here's how (a, b, c... etc.) Rust will make you even better at what you do.
Another point, it's safe to say C isn't going away anytime soon, it sits nicely at the next approximately natural level (in abstraction) above assembly. That was a design point in the language. Yes it can be a rough tool to use, but that's a reality when you get closer to the metal. You get tremendous flexibility and that comes at a price, as all things do.
To me, modern tools like Rust are supposed to relieve part of the burden. In the common case, if I can be reasonably certain that I can't make certain classes of errors, my brainpower is freed to focus on other things and I can work with significatly less stress.
Rust is interesting because it allows you worry less without actually compromising much in capability. In the cases where I would need to be able to bypass the tooling, it is explicitly possible.
If there were as many blown off feet as comments on HN suggested technology even as it currently is simply wouldn't function.
Do you even understand how much mission and safety critical software is written in C? I think if you did you'd either have a constant panic attack (given your apparent belief that it's impossible to write "safe" C) or else have to adjust your world view a little bit.
(I still agree that results at that point will be more interesting then speculating today.)
I don't think you get to play on both sides of that fence. Some of your team has been working on Rust for that long, but I doubt any code with sigils compiles, and I doubt there were many large projects using it then. I started my stopwatch at May 2015.
> Ten years is not that long a time.
Totally agree.
> I still agree that results at that point will be more interesting then speculating today.
We'll see on May 2025. :-)
My response was to a commenter who, as far as I can tell, represents the mainstream Rust community's view: that it's impossible to write safe C or, worse, that infallibility is a reasonable standard to apply. The latter is especially vex-some from my point of view, because Rust is not infallible, I happen to find it (so far) much more pleasurable to use than either C or C++, and I would very much like to see it have broader adoption.
According to steveklabnik, about 50% of Firefox's critical security bugs were due to memory safety issues (source: <https://www.youtube.com/watch?v=_-fweBvtifA&t=27m20s>)
Presumably you don't think mozilla exclusively hires shitty developers.
The tooling seems nice, but far from unique (D, Go and other languages are quite competitive there). It would be far more interesting to hear more about, say, traits (can it compete with C++ in compile time polymorphism?) and other technical aspects that make Rust stand apart from its competitors.
I am also extremely fond of pattern matching and algebraic data types (first class tagged unions). It is a style of data modeling borrowed from ML/Haskell and is quite distinct from OOP. The key distinction is that by making a datatype with multiple variants closed to extension, you can clearly see and handle all possible cases for a particular purpose (with checking from the compiler) which is often more natural then having fragments of implementation across many child classes.
Safety is a cool feature, but Rust is more than just safety, and pitching it as 'rust is safe' when it's not (and, often, it's not...) and when people dont care about that, doesn't convince people.
There are a lot of people who blog about rust, and almost invariably the 'killer feature' they come up with is safety.
It's not the killer feature; it's the side effect of strong memory management without a GC on an excellent general purpose programming language.
We could certainly do with more posts about how great and practical rust is, what the state of rust tooling is, and what great rust libraries there are with no mention at all of safety.
(... because safety is a cool technical feature; but the others; tooling, libraries and productivity are active barriers to adoption)
If you have to know a lot about Rust to find out why you're using it in the first place, you certainly do have a chicken-or-the-egg problem. And, if the promotion doesn't leave you with a clear idea of why you might want to use Rust, that promotion couldn't have been very successful.
In any case, it's true that one shouldn't attempt to make an opinion about a technology without first being well informed with that technology, and that requires spending time with it to determine the validity of one's own hypothesis about it. Speaking down about it without having tried it is rather offensive to those who have spent the time to learn it.
Rust is strongly about memory and thread safety, which is a major plus. I don't see how that would cause it to be ignored by large swathes of developers when, in fact, that's what is causing large swathes of developers to make the transition. Even long time C houses like GNOME and Red Hat are calling out to stop writing future software in C, and to instead use Rust. GNOME's put their money where their mouth is and they are beginning to migrate their C codebases to Rust. librsvg, for example, is now written fully in Rust.
Rust's safeness allows me to feel comfortable working with really heavy optimizations that make extensive use of fat pointers everywhere to mitigate memory costs and keep everything on the stack. I can feel confident that if my solution compiles, it works. That Rust features test-driven development built into the language by merely adding a #[test] line above a test function to ensure that my logic is correct is even more comforting.
Yet there are many more arguments to make for Rust besides the memory safety. The memory safety is a side effect of the type system. The type system allows for more kinds of safety than just memory safety alone. It allows for compile-time checking of state machines, eliminating a large number of runtime checks at compile-time when you know how to take advantage of it.
Graphics APIs like OpenGL and Vulkan, for example, exhibit much cleaner APIs without the need for performing runtime checks to ensure that instructions are called in the right order, enabling faster execution out of the box.
Basically, taking a state by self will drop the original state, whereby you can return a new state in its place. Code completion will additionally aid the developer in only displaying methods that the current state supports. You can find examples in the hyper and vulkano crates.
In addition, Rust is heavy on the concept of data-oriented programming via protocol-oriented programming: traits featuring ad-hoc polymorphism akin to the likes of Haskell. This encourages more efficient programming practices than the object-oriented approach found in higher level languages like C++. Traits allow for a powerful form of generics whereby you can specify a range of input/output type parameters for all types that support the included traits.
The Iterator trait is by far one of my favorite features of Rust, and it is available without the standard library where it is absolutely vital. Basically, by implementing the Iterator trait for a type, which entails merely implementing the Iterator's next method, you gain access to all of the Iterator's adapters, which opens the door to all of Haskell's best features -- lazy functional programming, but without requiring a garbage collector and without using the heap. It boils down to very efficient machine code compared to if you had written it using a loop.
The sum types and pattern matching is also one of my favorite features of Rust. It's also key in the creation of powerful custom Iterators that may return multiple possible outputs, and it's one of the best parts of Rust's error handling strategy.
If a function may or may not return a value, then the return type is an Option which may either by Some(value) or None. Iterators return Options. If a function has a possibility of an error, then the return parameter is a Result which defines either Ok(value) or Err(error).
Enums may have their own parameters too, so pattern checking can become quite comprehensive to cover every possible result, which may have completely differing input parameters. One enum field could return an &str, another could return a usize, another could return an &str and a usize, and they can contain other enums with their own fields as well.
match result {
Ok(Token::Argument(string) => (),
Ok(Token::Placeholder => (),
Err(TokenErr::IO(why)) => (),
Err(TokenErr::NoArguments) => (),
_ => (),
}This then allows concepts like CoW smart pointers to be represented as an Cow::{Borrowed, Owned} enum in the standard library.
Anyway, language features aside, of which there's many more important features that Rust has to offer that'd take too long to explain, there's also the tooling that makes Rust powerful.
The rustup toolchain is one of my favorite things about installing and managing Rust on a system. It's trivial to install, and yet makes installation and updating stupid simple, regardless of your platform. I can easily tell others how to compile my software without them having to know anything about Rust or finding packages in a software repository.
rustup toolchain install nightly
rustup update (updates all toolchains)
rustup target add x86_64-unknown-linux-musl
rustup target add x86_64-pc-windows-gnu
rustup component add rust-docs
rustup component add rust-src
rustup run nightly cargo install racer
rustup run nightly cargo install clippy
cargo build --release --target x86_64-unknown-linux-musl
cargo build --release --target x86_64-pc-windows-gnu
rustup docs (opens offline version of Rust documentation website)
All from the same box. Cross-platform development made easy.
Then there's the powerful capabilities that cargo provides itself. It has a plethora of built-in subcommands, and exports a public API so that you may create and distribute additional subcommands. It automatically creates project hierarchies for you, even initializing git for you.
The Cargo.toml file allows you to define conditional compilation features for your project and libraries that you are importing. It allows you to define what dependencies that you want to pull and from where you want to pull them. Merely specifying a dependency name with a version number will have Cargo search Crates.io for the corresponding library and compile it when you build your project.
For binary projects, a Cargo.lock is provided which notes the exact version and hash of each library that you built to ensure that others that build your software will build it with the exact version libraries that you did when you released it. You only need concern yourself with dependencies on user systems when importing C libraries. If you build on Linux with the MUSL target, you can even build fully static binaries with zero dependencies, so long as you either don't use any C libraries, or build those libraries with MUSL.
Subcommands may even use their own tables in the Cargo.toml file to specify extra behavior, such as my cargo deb subcommand, which requires a few more fields to package Rust projects into Debian binary archives. Cargo provides many interesting subcommands, such as:
cargo build
cargo install
cargo run
cargo check
cargo update
cargo publish
cargo search
cargo doc
cargo edit
cargo test
cargo bench
cargo flame
cargo profile
cargo watch
cargo deb
cargo rpm
and probably more that I'm not yet aware of..
There's much to be said of Rust's community and documentation as well. Never before has a language had such comprehensive and ambitious effort as Rust into establishing an official community with a wide range of community resources. There's an official Rust Docs team that's continuing to add more documentation and examples for Rust and top crates in the Rust ecosystem; then there's a team dedicated to authoring official markdown-based books to teach Rust as the go-to free, printable book for Rust; there's an official Reddit thread, an internals discourse forum, a users forum, multiple IRC channels, and apparently quite a number of key developers browse through and comment on this domain as well.
Seriously. However, I've seen a lot of comments to that effect from C and C++ programmers who don't want memory safety.
I come at it from a different angle: I'm a Java developer, so I already have memory safety. Pointer arithmetic, dangling pointers, and buffer overflows are not allowed, there is a garbage collector, and so on. In fact, most of the currently popular languages other than C and C++ are memory safe in similar ways. So, in terms of memory safety, Rust does not give me anything critical that I don't already have.
When choosing among the memory-safe languages for a real-world project, I'm going to need a pretty compelling reason to choose Rust instead of Java, Scala, Go, Ruby, Python, etc. -- especially when it is so much easier to hire developers for more popular languages.
I'll take a shot at some of the advantages:
Algebraic datatypes: Seriously. These are amazing. C++ has them with Boost's variant (and now std::variant), but they're awkward to use, making them somewhat a niche instead of the very central place they take in Rust programming. Till now ADTs were mostly the domain of functional languages, so I'm very happy that Swift and Rust giving them first class support and making them essential.
ADTs in Rust work with the enum keyword:
enum Shape {
Rectangle(Point, Point),
Circle(Point, u32)
}
let some_shape = Circle(Point::new(1, 2), 5);
match some_shape { // like switch, but with pattern matching
Rectangle(p1, p2) => println!("Rect from {} to {}", p1, p2),
Circle(p, radius) => println!("Circle at {} with radius {}", p, r)
}
An enum is essentially an "or" type; i.e. "a Shape is a Rectangle (X)OR a Circle". You are forced to handle this fact when you access this data -- if you try to access the stuff inside some_shape, you must match (or use a method that does this for you), and the match must be exhaustive (cover all cases). This is how null works in Rust -- if you want to say something is nullable, you use `Option<T>` -- `Some(T)` for when it's there and `None` for when it's null. You're forced to check for the none case if you want to be able to get to the inner data. You can call `.unwrap()`, but internally that's a method that just does a match and panics in the error case.Traits: I mention this elsewhere in the thread, but Rust metaprogramming via generics cannot and will never beat C++ TMP's level of expressivity. Rust is going to get procedural macros which make it easier to fill in that gap of metaprogramming in a less hacky way, but if you just compare generics and templates then templates can do a lot more. However, that's not necessarily a good thing. The way templates are structured; they basically get "monomorphized" at something akin to parse time. This means that the error messages can be atrocious, and an API can never be self-documenting since you have to explain what kinds of types can be passed in. On the other hand, in Rust, if you get a function from a library you can pretend that it's a black box. The error messages will never mention the contents of the box. You can adequately figure out how to not make the compiler error out by looking at the type signature. The way Rust traits work is that you first define them:
trait Frob {
fn frob(&self, frobbee: &str);
}
Then you implement them: impl Frob for SomeType {
fn from(&self, frobbee: &str) { println!("{} frobs {}", self, frobbee) }
}
Now, you can write functions (or types, or methods, or whatever) which accept these: fn frob_something_10x<T: Frob>(frobber: T, frobbee: &str) {
for _ in 1..10 {
frobber.frob(frobbee)
}
}
If I did not have the `T: Frob` bound, I would not be allowed to write this method; the compiler would say that type `T` doesn't have a frob method, and I'd be forced to add a bound that gives it one. If a library user passed the wrong type to the function, the compiler will tell them that their type doesn't implement Frob, at which point they can add an implementation or use a different type or whatever. It's a very nice, clear API separation which leads to clear error messages and makes it easier to figure out what kinds of types to use. It also means that in the autogenerated docs I can just click on the trait to find out what types I can feed to the function -- in C++ many codebases have their own bespoke "trait" system using template specialization but it won't work with the docs and still lends itself to rabbit-hole-y error messages.Moving: I personally find the lack of copy/move constructors and default construction to be very nice. In Rust, uninitialized types aren't a thing; initialization is always explicit. Copies are just copies, moves are also just copies, and moving is the default (except for POD types). There's no unknown overhead to deal with when I push to a vector, for example. Move-by-default and affine types IMO make it very easy to think about the program.
On first glance, I can see some utility in the ADT matching, but not necessarily something I miss in C++ (std::variant + templates gets me most of the way, although there are some issues with that approach). On the other hand, the clean separation of concerns and clear syntax of the traits example looks very useful indeed, and quite tempting.
That is how we end up with the miserably insecure state of affairs of today :)