I’m not being sarcastic, in all earnestness educate me!
I’m not being sarcastic, in all earnestness educate me!
1) it requires a later execution, rather than your code failing to even compile.
2) it requires excellent and complete tests, probably with quality fuzzing, as often the corner cases (like hiting buffer size limit) is where problems occur.
For me, the argument for Rust is exactly that it let me (mostly) get rid of ASAN and UBSAN and valgrind and friends, which I consider required for C++.
The other big advantage of Rust is package management, with I feel C and C++ are awful at, particularly if you want to release for Linux, Mac and Windows.
I think I consider the need to run under ASAN in a configuration that gets code coverage a feature rather than a bug. It's stupid monkey-brain stuff, but the forcing function of needing to be able to drive your critical path at will in my experience leads to better outcomes.
But since Rust's Memory Safety guarantees apply to components, the composition of those components also has Memory Safety by definition.
If you have one guy writing incredibly scary to-the-metal unsafe Rust to squeeze out 1% more performance from a system that's costing 85% of your company's burn rate, you can put that tiny component through ASAN hell, looking for any possible cracks in its Memory Safety with all sorts of crazy input states.
But the sixteen other people in the team writing stuff like yet-another REST JSON handler entirely in Safe Rust don't need that effort and when you compose all these pieces together to build the actual product, you don't need ASAN again, you checked that the scary bit was safe and you're done.
Also, even if Rust only caught by itself what Clang and half a dozen analyzers would catch, you also get a really nice language as a bonus. There is much more to Rust than just safety.
The by-design thing I get for like something with a port open, but as a default? It's been a few months but the last time I was trying to hack a Rust project printing stuff out hit the borrow checker. I can live without that.
Rust is admittedly a language you can't just jump into without spending some time learning the fundamentals. If you try to do anything non-trivial without grokking the basics of ownership and borrowing you are bound to run into frustrating issues.
I like learning programming languages, I like working on compilers, this stuff is basically my only hobby.
But as the TF2 people are learning the hard way, if a complete novice can’t get a log line in without understanding a bunch of novel context, PyTorch is taking your SO out to dinner pretty soon.
Now if Rust could build a million lines of code in 1-5s like it should be able to (and like it seems that e.g. Jai can) I wouldn’t care. I needed to get stuff done in C++ bad enough that I learned template metaprogramming, which I wouldn’t wish on an enemy. If Rust made my builds 50-100x faster in low optimization settings, I’d write it on clay tablets.
But what I actually get is a comparably slow build, a comparably “!$&@>>>>”-heavy syntax, yet another failure to do macros that impress a Lisper in a curly brace language, and now the static analyzer’s usually good advice is mandatory and can’t be turned off to do a quick experiment.
I always have to watch my cynicism because I’m a bit autistic and I offend way more often than I mean to, but if I’ve got 1MM lines of C++, I’ve got a pain in the ass on my hands. If I start porting any meaningful part of that over to Rust, now I’ve got two giant pains in the ass.
Additionally during development a full build is usually not necessary all the time; oftentimes it's just enough to check the code for errors with `cargo check`, which is usually significantly quicker than building a full debug binary.
Edit:
I tested building Alacritty from source on my computer (Ryzen 5600X, Windows 10). The initial build (consisting of 130 crates) took 34 seconds. Building again after adding a single print statement took 6 seconds, and `cargo check` took about half at 3 seconds. It's not ideal by any means, but IMO it's not also unreasonable for 25K lines of code, plus those ~130 dependencies.
Not saying this is true about Rust, but I find these approaches dangerous because they can move bugs off to a different meta level where they're harder to detect.
For instance, some people make safe C dialects where signed integer overflow wraps around or variables are auto initialized to 0. Well, what if you didn't want to overflow at all or 0 was the wrong value? Now you can't find the bug because the program has been redefined to do the wrong thing instead of simply crashing.
Most of Rust's safety features are handled at compile time, and they are usually handled explicitly, not implicitly. A few examples about initialization:
* An instance of a struct is created by giving an initial value to each field. Leaving out any field is a compile error. Since structs are always completely initialized, you can't accidentally pass references to partially initialized structs like you could in a C++ or Java constructor.
* All local variables must be initialized. Accessing a variable before initialization is a compile time error.
* Structs have no implicit default value. The default value can be defined by implementing a trait, either by yourself or by deriving it with a macro (which would default integers to 0 and so on). Default values are only used by a fixed set on functions in the standard library, and never implicitly.
The overflowing case is one of the only cases where Rust is a bit more implicit. Integer overflow is checked at run time in debug builds (so over/underflow causes a panic) and defined to wrap around in release builds. However, the standard library also defines methods on numbers which explicitly use wrapping, saturating or checked (either return a Result value or crash) semantics.
I don't know enough about Rust to comment on whether it does a better job than C++ on reference cycles, but my suspicion is that it does.
[0] https://www.chromium.org/Home/chromium-security/memory-safet...
[1] Related discussion: https://news.ycombinator.com/item?id=26861273
AFAIK avionics software is still largely written in Ada, because it won't let you fuck up meters vs feet type stuff. And if someone said: "Rust has a slam-dunk niche: we're going to crank static analysis past helpful to downright intrusive because sshd simply can't buffer overflow", I'd be like, yeah, ok.
But at the time I stopped using it, Alacritty couldn't handle meta keys on Big Sur, and I wanted to fix that, so I spent a weekend or two that I really couldn't spare trying to unfuck it, but between `print` not being obvious (because someone had already borrowed the thing I wanted to print out) and the build being slower than C++ I timed out.
If they can’t get it right, who can?
My suspicion is that this is too optimistic, but I can't really substantiate it.
What would be a good security-sensitive modern C++ codebase, ideally from a high-profile source like Google, to compare against?
Of course this is not an answer to your comment.
Even if you use std::unique_ptr you can still run into use after free bugs. std::unique_ptr is just heap allocation with a "stack allocation style" lifetime. You can still run into use after free by putting a reference to stack allocated data (this applies to std::unique_ptr as well) into a struct or global variable that outlives the stack allocated data. std::unique_ptr isn't really meant to solve use after free, it exists to ensure that heap allocated data is properly freed eventually. It saves you the discipline to match every new with a free but nothing more.
This is something that you can only solve with a borrow checker. If you were to add the ability to detect these types of problems in C++ then you would have essentially added a borrow checker to C++.
Here's an example from one of Herb Sutter's talks, which I refer to frequently. He describes an issue that can come up with shared_ptr in reentrant/callbacky code, and the punchline is that you have to be careful never to dereference a shared_ptr that might be aliased: https://youtu.be/xnqTKD8uD64?t=1380
I also just wrote a giant example of my own :) in reply to your toplevel comment. https://news.ycombinator.com/item?id=27168368. TSan will be pretty good at catching some of the simple variants of this, like where you totally forget to take a lock. But it's going to have a hard time with cases where you "stash" a reference longer than you should, because later uses of the reference might appear safe for a while yet become unsafe down the road with unrelated code or timing changes.
Another issue with ASan and UBSan, is that every application has to run them (and maintain its own test suite comprehensive enough to make them useful). With static checks, a memory-safe library is memory-safe for everyone, even for applications that aren't particularly careful with their tests. The library author is able to express their safety requirements through the language, and all callers have to respect those requirements all the time.
Rust's borrow checker makes it impossible to write code containing a data race. It does this by only accepting a subset of possible safe programs, but this is (or at least may be) a reasonable trade-off due to the guarantee you get.
It's not that you can't write safe code in C++, but you're always at the mercy of your validation to try to have confidence that you've actually managed to do what you set out to do. While Rust obviously doesn't preclude bugs at all, but it does enforce that you _can't_ write a certain class of previously-prevalent bugs.
I think this blog post from Mozilla does a good job of what that means in practice: https://blog.rust-lang.org/2017/11/14/Fearless-Concurrency-I...
I think this is correct but would expand on the nature of the tradeoff. Rust only accepts a subset of safe programs, and can express any program in the abstract, but that subset may not contain the optimal safe program. In some cases, differences in practical performance between the expressible safe program and the optimal safe program can be quite large.
Modern C++ cuts a tradeoff along a different dimension: it will accept unsafe rubbish but the optimal safe program is also expressible in virtually all cases. C++ has large niches where it can express optimal safe programs, and where this optimality is important, that Rust cannot. Obviously it is incumbent on the programmer to write safe code in C++, but the flexible type system is more capable of enforcing safety than I think people expect, particularly in recent versions of C++.
But there's optimal and there's "optimal" -- by restricting the developer, we open up more opportunities for the compiler to optimise the code (for all that Rust isn't necessarily able to take advantage of these opportunities yet) and a judicious use of `unsafe` may even help us to build an abstraction that gives us the optimal program anyway. Crucially, code that uses unsafe is still no less safe than ordinary C++ code.
From my days as a compiler engineer, I definitely prefer to write clear code and fix the compiler rather than try to convince the compiler to produce the right result by mangling code. And to build my control logic as an abstraction, even if it's only going to be used in one place, so it can be verified separately from the business logic it drives.
https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=Rust+data+r...
A quick check suggests that (as would be expected) they're caused by `unsafe` blocks. Specifically, by the developer claiming that a value was safe to send to a different thread while it actually wasn't. One thing to note is that these bugs neutralised the compiler's validation is these specific cases -- allowing, rather than causing, races. Using unsafe means the developer needs to ensure they maintain the invariants themselves within the unsafe block, while in C++ one has to maintain these invariants in _all_ code.
I mean C++ dependency management is so bad there is a concept of a header only library so you just have to copy the file into your project with no support for managing versions.
C++ dependency management is so bad people use the OS installed dependcies because they struggle to set up a build environment otherwise.
What I'm saying is that C++ is pretty good and toe to toe C++ with all the tools you describe and Rust are fairly comparable. But with Rust the whole ecosystem uses it so the network effects make things much better. And it's snowballing.
I actually think this is one of the things that Rust could crush C++ on, because build times are becoming the whole show on big C++ projects.
It's super weird to me that C++, with this whacky 1970s constraint that totally fucks up modularity, still builds neck-and-neck or better with Rust.
In principle Rust could do *way* better on this, and that is a feature that might get me to consider Rust seriously, no matter how stupid I think the borrow checker is. But Rust still compiles dog slow...
Not so fast! The compilation and linking is neck and neck with Rust so you can have an ELF in the same amount of time. But then you have to run all the C++ tools you mentioned to have parity with what the Rust compiler is doing.
> for all the talk about C++20 modules they haven't delivered yet.
Even when they're delivered, will all the libraries you depend on use them?
>In principle Rust could do way better on this, and that is a feature that might get me to consider Rust seriously, no matter how stupid I think the borrow checker is. But Rust still compiles dog slow...
The team that built the Borrow Checker deserves the Turing Award. It is an outstanding bit of computer science and engineering. I'm surprised that you think it's stupid.
It used to be dog slow but it's gotten very fast in the past two years. Another achievement.
>In principle Rust could do way better on this, and that is a feature that might get me to consider Rust seriously
Modules? Your take away from all this is that modules are the killer feature of Rust and that the Borrow Checker is stupid?
I call shenanigans. You are an elaborate troll. Well done for getting two responses out of me.
On the security standpoint and a wide net of crates.. that's a problem, same as with npm/yarn/pip/whatever and I agree on that. That one bugs me as well. Difficult to audit.
Edit: this post keeps getting up and down voted. I suspects it has to do with the phrase infected by gplv3. I like the license fine, but being realistic many businesses treat it like radioactive waste, so that has to be a consideration
vector<int> v{1,2,3,4}; for (auto&& i: v) v.push_back(i);
Again, in the spirit of becoming more educated, what's the equivalent Rust code? I have `rustc`/`rustup` on my box so I can run that too.
It gives you a small example of what the error is, explains it, and tells you how to fix it.
This is a very nice feature for new people (like myself).
In C++ you (usually) do this with `const`, which is fairly low friction, very statically analyzable, and I'm a little unclear what bug hardcore borrow-checker static stops me or my colleagues from making that `const` can't catch?
Rust catches the bug where you forget to use `const`, because the C++ compiler doesn't force you to use it. Your argument boils down to "I don't need Rust's borrow checker, I just have to remember to use a dozen tricks, follow various patterns and guidelines, run several static analyzers and runtime checks, and I'm almost there".
This is not meant as an attack against you. I used to program in C++, and I'm mostly using Rust now. The borrow checker frees up those brain resources which had to keep all those tricks, patterns, and guidelines in mind.
But to directly answer your question, suppose I have a “struct FloatArrayView { float* data; size_t size; };” Of course this is a toy example, but humor me for now and consider the following:
1. Without excessive custom effort (e.g. without manually unwrapping/wrapping the internal pointer), how do I pass it to a function in C++ such that the data pointer within becomes const from the perspective of that function (i.e. you’ll get a compile error if you try to write to it?) Hint: It’s very complex to do this in C++, vs trivially easy in Rust. And no, passing as const or const reference to “FloatArrayView” does NOT work. The only solution in C++ for complex composite types operating with “by-reference semantics” is ugly, complex, and horribly error prone to maintain correctly. For a more concrete example, consider how “unique_ptr” works with the constness of the value it holds. A “const unique_ptr<T>” is NOT a unique pointer to const T. A “unique_ptr<const T>” is, but the relationship and safe conversions between these are not simple or easy to implement or even use in many cases. It gets even worse when you need to implement a “reference semantics” type like this that is inappropriate to be a templated type, or contains a variety of internal references unrelated to the template arguments.
2. Now suppose I solve #1 and pass this into some class method, which then stores a copy of the const reference for later. But I as the caller have no way of knowing this. So after that method returns, I later proceed to modify the data via my non-const reference (which is a completely valid operation), but this violates the previous method’s assumption that the data was immutable (will never change). This creates an incredibly dangerous situation where later reads from that stored const reference to data (that was assumed to be immutable) is actually going to actually yield unpredictably changing results. Const is not immutable. Question: How do you make it so C++ guarantees that passed reference is truly immutable and will never be mutated so long as the reference exists, and enforce this guarantee at compile time? In Rust this is easy (in fact, it’s the default). In C++, it is impossible. The closest you can get in C++ are runtime checks, but that’s nowhere near as good as compile time checks.
Edit: Removed a bunch of perhaps unnecessary extra C++ trivia which I’ll save for later :)
To be more clear, in Rust, the `mut` you see in bindings (like `let mut blah = ...`) is wildly different from the `mut` in `&mut T`. There was discussion on whether the latter should be renamed to `uniq` (because `mut` is misleading). In any event, Rust lacks `const` types. There is no `const T` like in C++. So something like this is impossible,
std::vector<const int> vec;
You can't have just the elements of a collection to be immutable like the above example in Rust.Secondly, a value of a type `&T` can be mutated internally (aka interior mutability). A function taking a `&T` type and mutating it behind the scenes is very whacky. AFAII, interior mutability exists only to trick the borrow checker using the `*Cell` types (which use unsafe internally BTW).
Imagine all the best tools and all the best and memory safe features all rolled up to one called Rust.
Lastly, with C++ there are many situations where the memory unsafe actions are not caught until they are triggered dynamically. So like others have mentioned, you’d have to have the best test suite combined with the best of fuzzing etc.
Or you can just use Rust where nearly everything is caught at compile time with useful errors and when something funky does happen at runtime it will just panic before you cross into UB/ unsafe land
If this doesn't seem like a game changer to you, then you probably don't grok its scope and impact. I don't really know how to give that to you either in HN comments. I think you maybe need a synchronous dialogue with someone.
Assuming the absence of `const_cast`, C++'s `const` ensures that a data of the particular type doesn't get modified. That's it. Rust on the other hand, tracks and restricts aliasing. This means you can't get both a constant and non-constant reference to an object or anything owned by it (more precisely anything transitively reachable from it). This is helpful in preventing issues with invalid pointers (like iterator invalidation). For instance, if you have already borrowed an iterator (which is essentially a reference) you can't issue mutable operations on the vector (issuing mutable operations entails a mutable borrow), and that could potentially reallocate the backing memory of the vector. This property is also useful for preventing multiple writes to the same location in a multi-threaded application, making programs adhere to the single-writer principle by-construction.
The downsides are that you might sometimes need to contort your code or your program architecture or perform mental/type gymnastics to write some trivially safe code or even use unsafe (or else lose performance) to satisfy the borrow checker.
#include <vector>
int main()
{
std::vector<int> foo{1,2,3,4,5};
foo.reserve(foo.size()* 2);
for(auto it = foo.begin(), end = foo.end(); it < end; ++it)
foo.push_back(*it);
} fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let new_size = v.len()*2;
v = v.into_iter().cycle().take(new_size).collect();
println!("{:?}", v);
}
https://play.rust-lang.org/?version=stable&mode=debug&editio...- Repeating the vector using the built-in method `Vec::repeat`. This allocates a new vector, but `reserve` is likely to do the same, just implicitly.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
- Using a traditional for loop. There are no lifetime issues, because a new reference is acquired on each iteration. The reserve is not required, but I included it to match your C++ version. https://play.rust-lang.org/?version=stable&mode=debug&editio...
- Creating an array of slices and then concatenating them into a new Vec using the concat method on slices: https://play.rust-lang.org/?version=stable&mode=debug&editio...
#include <vector>
void do_stuff(int &a, int &b) {
// stuff
}
int main() {
int my_array[2] = {42, 99};
do_stuff(my_array[0], my_array[1]);
}
That is, how do we take two non-aliasing mutable references into the same array/vector/view/span at the same time. (To be clear, none of the following applies to shared/const references. Those are allowed to alias, and this example will just work.) Notably, the direct translation doesn't compile: fn main() {
let mut my_array = [42, 99];
do_stuff(&mut my_array[0], &mut my_array[1]);
}
Here's the error: error[E0499]: cannot borrow `my_array[_]` as mutable more than once at a time
--> src/main.rs:7:32
|
7 | do_stuff(&mut my_array[0], &mut my_array[1]);
| -------- ---------------- ^^^^^^^^^^^^^^^^ second mutable borrow occurs here
| | |
| | first mutable borrow occurs here
| first borrow later used by call
|
= help: consider using `.split_at_mut(position)` or similar method to obtain two mutable non-overlapping sub-slices
The issue here is partly that the compiler doesn't understand how indexing works. If it understood that my_array[0] and my_array[1] were disjoint objects, it could maybe deduce that this code was legal. But then the same error would come up again if one of the indexes was non-const, so adding compiler smarts here wouldn't help the general case.Getting multiple mutable references into a single container is tricky in Rust, because you (usually) have to statically guarantee that they don't alias, and how to do that depends on what container you're using. The suggestion in the error message is correct here, and `split_at_mut` is one of our options. Using it would look like this:
fn main() {
let mut my_array = [42, 99];
let (first_slice, second_slice) = my_array.split_at_mut(1);
do_stuff(&mut first_slice[0], &mut second_slice[0]);
}
However, other containers like HashMap don't have `split_at_mut`, and taking two mutable references into e.g. a `HashMap<i32, i32>` would require a different approach. Refactoring our code to hold i32 keys instead of references would be the best option in that case, though it might mean paying for extra lookups. If we couldn't do that, we might have to resort to `Rc<RefCell<i32>>` or trickery with iterators. (This comment is too long already, and I'll spare the gory details.)At a high level, Rust's attitude towards multiple-mutable-references situations is leaning in the direction of "don't do that". There are definitely ways to do it (assuming what you're doing is in fact sound, and you're not actually trying to break the aliasing rule), but many of those ways are advanced and/or not-zero-cost, and in extremis it can require unsafe code. Life in Rust is a lot easier when you refactor things to avoid needing to do this, for example with an entity-component-system sort of architecture.
And that doesn't work, because the v.iter() is sugar for Vec::iter(&v), while the v.push(i) is sugar for Vec::push(&mut v, i) . I think it'd use deref coercion on the i, as Vec::iter(&v) gives you `i: &isize`. If this wasn't ints (or other `Copy` types, for that matter), you'd need to use .into_iter() to consume the Vec and get ownership of the entries while iterating, or use `.push(i.clone())` because `Vec<T>::push(&mut self, T)` is the signature, and you can only go automatically from `&T` to `T` for `Copy` types. Actually, it _may_ even need `v.push(*i)`, thinking about it. Try on https://play.rust-lang.org
C++ is actually a pretty gradually-typed language, and I'm in general a fan of gradual typing. I don't mind that some people prefer BDSM-style typing, but IMHO that goes with GC e.g. Haskell a lot better than it does with trying to print something out or copy it or whatever.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
Alternatively, you can loop over indices instead of values, which doesn’t require the loop to maintain a reference to `v` across iterations:
https://play.rust-lang.org/?version=stable&mode=debug&editio...
All the tooling you've just listed adds build complexity, on top of a build system that is already an abomination of makefiles, CMakeLists and lack of package manager. Let's not forget all the header files duplicating half of your implementation.
Batteries included with safe defaults is a BIG feature.
Legacy C/C++ code is littered with so many memory related bugs that big companies are funding people to rewrite essential unix utils in Rust.
Modern C++ vs. modern Rust. Why is modern Rust better? Legacy C++ can be transformed into modern C++ is a semi-mechanical, semi-you-emply-someone-good-at-emacs way, at a cost in time, money, and risk way less than empty editor.