Why choose Rust over C or C++?

Please do see: Jon Gjengset's presentation: "Considering Rust" - https://www.youtube.com/watch?v=DnT-LUQgc7s a very reasoned and unbiased description of the pros and cons of Rust and why one might want to consider it.

Personally, as a long time user of C/C++, I'm adopting Rust because:

  • Correctness - Rust's type system and memory usage correctness checks prevent be from introducing all kind of bugs that will waste my time after deployment. This also has implications for improving the security of ones code.

  • Performance - Rust provides performance that matches C/C++ and sometimes surpasses it.

  • Applicability - With no run-time system or garbage collector requirements I can use Rust all the way from embedded micro-controllers to server side to browser (not that I have looked into that last one much yet.

  • Ease of use - With Cargo and the Rust crates it is so much easier to make use of all the wonderful libraries people have created. Rust is a much higher level language than C but without all the mind numbing complexity of C++. You don't need to know it all to start getting useful things done. Importantly what you don't know cannot cause you problems as it can in C/C++.

  • Community - Rust is a big language. It has features and implements concepts that are likely new and unfamiliar to users of C/C++ (or other languages I guess), the community has proved very supportive and helpful.

If these are things you value then you may well like to consider Rust as well.

21 Likes

You might get different answers depending on:

  • is it your hobby/one-off project or is it something you're introducing in your organisation?
  • is it a new project, is it a rewrite or is it extending existing project?
  • are your collaborators willing to switch?
  • is both C++ and Rust new to you or do you already know one?

I've been doing C++ for 20 years, Rust less than a year and professionally only 2 months. My observations so far:

  • a lot of type safety coming from Rust can be done in C++. Controversial, I know. In C++ however it's opt-in and you must be disciplined but it's a matter of habit in the end. In my case Rust doesn't bring much new here, it just makes it easier to express certain things.
  • that said, where Rust is way better than C++ is preventing data races so if you have multithreaded code, Rust gives you guarantees that C++ cannot. This makes code reviews much simpler. It's easier for onboarding new team members since simple mistakes are impossible.
  • external libraries and dependency management is a mess in C++. There are too many not-quite-compatible ways. Things seem to be settling on CMake+conan but it's nowhere near as neatly integrated and easy to use as Cargo. Here Rust is way ahead of C++.
  • similar situation is with testing, Rusts integrated approach is much more pleasant to use.
  • I've seen it mentioned a couple of times but I don't find it particularly good/easy to integrate Rust into C++ code -- the common interface is C so it's not Rust <--> C++ but rather C++ <--> C <--> Rust which is a lot more work than generally suggested. In my case using microservices is an easier approach.

C++ has grown over the last 40 years and it's a mix of everything and you need to invest more time to learn about its footshooters. In many cases the most obvious approach in C++ is the dangerous one. A lot of outdated/bad advice on the internet doesn't help. So when starting from scratch, C++ is not the right language.

To me Rust is not really about what it can do but how can I do it. In general I find it easier to achieve things in Rust than C++.

12 Likes

Sanity. Hunting memory bugs in C/C++ might be fun for a while, but trying to hunt ghost bugs for weeks stops being fun.

7 Likes

I have been making the same question, specially considering I know C++ quite well.

At first read about it, memory safety made me want to learn more about Rust. But for me the key moment to consider Rust more seriously was to find that the rules also help on doing concurrency right. That, in my opinion, makes adopting Rust a no-brainer.

I also like how tests are more integrated into the language. In my ideal language we have not only a borrow checker, but also a coverage checker which stops compiling if unit test coverage falls below 80% :stuck_out_tongue:

Thanks ZiCog for suggesting the video from Jon Gjengset's. I will watch it.

1 Like

You already got some expert advice. As a newbie with only very little C/C++ knowledge I want to throw in, that the error messages I get from Rust are the best that I have seen in any language so far. It's more of a Rust-teacher-and-compiler. Depending on your situation that may count nothing or a lot.

Greets,
Bernhard

4 Likes

I find I agree with almost all of your post above. So I'm a bit surprised at that particular statement.

To my mind the whole point of the type and memory safety checks of Rust is exactly that it is compulsory rather than relying on the programmer to "opt-in" and be "disciplined" and acquire the "habit".

When you say "In my case Rust doesn't bring much new here, " you are saying you have opted in, you are disciplined and you have the habit. All the time. Never making mistakes. Perhaps so, but I find it unlikely that anyone is perfectly on their guard all the time.

The point of all that type and memory checking of Rust is that you don't have to be on your guard all the time, the compiler does it for you.

I think that is very new to the table. It's huge!

4 Likes

All the time, yes. Never making mistakes, not at all. :slight_smile:

What we do is we have rules around which C++ features we use and when, this is the discipline bit. For example we don't use raw pointers, we don't use uninitialised smart pointers, iterators are never returned in public interfaces, classes never take ownership of passed in references, etc. There are exceptions to these and that's fine, it's just a signal for everyone to discuss the reasons and details during a code review.

Once you get used to these rules they become a second nature and you design your code with these rules in mind so it's not really a cognitive burden anymore. In Rust you still have to design your code around borrow checker and you have to follow rules. The main difference is that it's the compiler doing the checks, not a fellow team member. I've heard a couple of times when Rust veterans say that even if they don't use Rust they design the code as if it was Rust, since that's generally better and safer way of writing applications. I guess we already use a subset of these when we came up with our "C++ rules".

I'm not suggesting that this works for everyone, it works for us -- to put in perspective, we had 3 out-of-bounds crashes since I started working here 12 years ago. We have about 600kloc of C++ and dozens of deployments running 24/7.

All that said, I do hope that our pilot Rust project will prove itself and we'll be able to sell it to the rest of the department. Rust has much better user experience and simplifies a lot of processes that we have in place to ensure we write correct code. If nothing else Rust would greatly reduce discussions when people say "Trust me I know what I'm doing". :slight_smile:

For me, C++ is getting over complicated as the time passes. Due to competition between different languages, C++ is trying to provide to developers what they want, instead of what they need. It's true that the language slowly converges toward safer code but will never reach it's goal due to compatibility with paleolithic times :wink:, and its inherent non-safety. Should you write perfectly safe code, you still can't be sure that the libraries you are using, are.

After a few weeks with Rust, I understand the 'Chris Dickinson from npm' quote, on the Rust website.

In my mind, it's not possible to evolve constantly. Sometime, you have to restart from scratch, with all previous experience in mind to get something nice. Here comes Rust.

[Peter's Avatar is beautiful. Is that JetSet Willy ?]

1 Like

I should never have doubted you !

Indeed. I'm confident that with a tightly knit team of conscientious and smart developers, with enough rules and guidelines in place, with enough checking, cross checking, reviews and testing in place, it is possible to reduce the rate of memory misuse faults to something approaching zero.

I have had the pleasure of working in such environments on a number of occasions, in military and avionic projects.

It has proved to be quite a rare environment though.

But all that is a lot of work. It's like back in the day we had huge teams of young girls doing endless calculations by hand, they were called "computers", and cross checking each others results. The motivation behind Charles Babbage wanting to build his computer in the 1800 and something, so as to reduce the errors in navigation tables.

It's time we got the computers to do all that tedious work for us. That is why we have computers is it not?

2 Likes

That's true. C++ so widespread and they need to keep the language backwards compatible. They keep deprecating stuff but it's quite a slow process. Nobody is going to invest in a language that keeps breaking things every 4 years. I'm wondering where Rust will be in 10 or 20 years, whether it'll suffer from similar compatibility problems.

To be fair, that's the case of any language, even Rust. How much do you trust your dependencies? Even without deps using ffi or unsafe code, they still can have deadlocks, not work on some architectures, have panics or ignore some errors, have hidden global state, etc. To me, borrow checker is part of the safety equation, not the full answer. Don't get me wrong, life is definitely better with it than without it.

Yes! :+1:

1 Like

This ruleset makes me wonder how you do things like "string views", aka a reference to an allocated string, in order to avoid both reference counting and deep copies? It seems to me like without raw pointers and keeping passed-in references, you cannot implement such views (except with reference counting).

I like that rust has lifetime analysis, because that enables patterns that are efficient, but inherently risky in C++. We have objects containing references to other objects in our C++ codebase, with documented annotations eg // lifetime(Foo) < lifetime(Bar). Of course, using a Foo after the corresponding Bar expired is UB.

The problem with reference counting is that it fundamentally doesn't statically retain information about ownership. Sometimes it's actually what you want when the lifetime is actually dynamic. But when it is not, I find it a bit sad to pay the reference counting overhead (I hope you use specialized pointers and don't pay an atomic inc/dec on each copy) and to lose information about which part has actual actual ownership and which parts are just observers.

I have similar questions about iterators: how do you abstract "containers", if you can't return a cursor to perform an iteration? Are you only iterating internally, Are you using java style iterators + macro sugar for nice for loops?

And yes, it may be possible to invest enough energy and time to derive safety from good practices and code review. However, by automating this part, rust allows to not spend that energy and time on safety, which allows to spend it on other parts of the project, such as design, error handling, tests, ...

It's like code formatting. It's possible to respect a uniform style, generally, if all the text editors have a sane configuration, and by reviewing for conformance with the coding style. But a code formatting tool simply removes the need to manually review "formatting". Just put it in your CI and wait for it to automatically review.

1 Like

Yes, top marks to "cargo fmt". That is another chore I don't have to waste time on. If everyone would use it with out of the box configuration it would save countless hours of heated argument over formatting style. And we'd have a world full of consistently formatted to code to read. Sweet.

Top marks also to clippy.

Generally we don't really have a need for string views. Typically strings are viewed as "values", not types so we have newtypes around strings and these are typically immutable and (typically again) don't require extracting substrings. It's not uncommon that non-newtype strings need to be (un)escaped so we'd end up copying the string anyway. Our strings are ref-counted as well and use small string optimisation with some interning. We use iterators/ranges but these are generally never returned from a function so it's much easier to keep track of lifetimes.

In our case we don't care that much about low-level optimisation, we focus on choosing an efficient algorithm first and making as few/efficient DB requests. So using Arc is fine even when Rc would do. In critical loops we dereference the pointer just once.

The rules as I wrote them were oversimplified, they're more like principles, not a rigid set of commandments. We have quite low dev turnover and we make sure everyone understands why these principles are in place and what's the benefit of using them.

Most of the containers are immutable/only contain mutable data but the container itself can't grow/shrink. We have some helpers bolted on top of them so if you have an array of e.g. Person objects, you can call everyone.getName() which calls getName() on each element and returns a new immutable array. We also have transformation/filter helpers over arrays. Creating arrays is relatively cheap, depends on elements. If it's a string, the cost is just incrementing ref counts of strings and creating a new vector/array with references. Most of the objects that are too heavy to copy are wrapped in an Rc-like object which also implements "Deref".

Totally agree. We came up with what we have during early 2000s and refined it over time. If we were starting now we'd probably still use the same design principles but instead of relying solely on people we'd let tools help us.

This is one thing that I think that has handled brilliantly. There idea of editions of the language that are ABI compatible but allow breaking changes to the API means that rust can fix its mistakes. We've only had one so far, but it went so smoothly and enabled major improvements that wouldn't have been possible while maintaining backwards compatibility.

1 Like

I like the idea of editions and I hope the future is bright. I'm wondering what the reality will be like, whether I'll need to stick to the edition that all my deps use (or the oldest dep uses) and lose out on new features the new edition brings. Or new editions will be backward compatible (but then Rust might suffer from the feature creep the way C++ does.)

I have very contradicting wishes. :slight_smile: I'd like a language that doesn't suffer from legacy features but at the same time I would like to have the option of using deps that use those legacy features.

The ABI is backwards compatible, which means you can transparently use crates that use an older edition, but the API is not backwards compatible, so code from the old edition will not necessarily compile with the new. There is a cargo fix that helps convert, though.

This combination means that library functions can be changed, and even library types, and the borrow checker can be improved in interesting ways. But no one needs to switch to the new edition unless they want to, and the switch is done piecemeal. I've been surprised to find years later crates that I created and continue to use that I just never bothered to move to the 2018 edition. They just keep working.

1 Like

This made me chuckle a little :sweat_smile: I once convinced a friend to check out rust by telling him "it shows snippets of the code in the error outputs, underlines the problematic bits in pretty colors and even suggests fixes to your variable name typos"

3 Likes

3 out-of-bounds crashes in 12 years? I am more used to see it like 3 per month :laughing: And for Java`s null pointer exception, 3 per week. :stuck_out_tongue: No wonder I am looking into Rust...

Current my job use cpp, python same time i learning rust. Rust and C can learn together .

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.