Hacker News new | ask | show | jobs
by benreesman 1422 days ago
Rust has a lot of great qualities that C++ lacks, but comparing `rustc` to `gcc` or `clang` on move-semantics checking is just kind of silly these days. `rustc` has `clang-tidy` built in.

`clang-tidy` is not letting you mutate or even access that moved-from "suffix" object without throwing an error.

It's annoying that you need `clang-tidy` and ASAN and shit to get comparable runtime safety even in greenfield C++, but that's not why to prefer Rust.

Prefer Rust because of a better traits system and pattern matching and syntax for `Maybe/Either` and a consistent build/library story and a number of other things.

4 comments

Your point is a fair one, but defaults matter. There's little reason for clang-tidy not to be part of the default clang invocation by now, other than an aversion to producing new output for existing projects (that you could argue are already "broken"). Unless clang-tidy has false positives, in which case the comparison isn't apples to apples then.
Oh I completely agree that defaults matter, and I wouldn't advise anyone to go start some big C++ project unless they had a very good reason to.

But Rust vs. C++ is not an apples-to-apples comparison in this regard: Rust got a clean slate and was willing to cut ties with engineer-millenia of existing high-value software to do it.

In a perfect world Rust wouldn't be ambivalent at best and hostile at worst to C++ interop, c'est la vie.

Rust isn't hostile about C++ interop, it's that between native interop (which requires dealing with templates and memory unsafety) and safety, Rust prioritized safety, while Carbon is trying the alternate approach. The behavior of C++ is simply hard to interface with while providing the assurances Rust gives you.

Edit: how many languages have native C++ interop that supports the whole language? Would love to hear of any.

I said ambivalent to hostile at worst, and I'm sorry, it is. I write FFI to C++ in Rust, Python, and Haskell practically every week, and Rust <-> C++ sucks.

Slap it in an `unsafe` block, fine. But let me move a `std::vector<std::string>` into my `unsafe` block easily. Erase the types, fine. But let me call `v.at(idx)`.

Python takes C++ interop seriously, which is why Tensorflow and PyTorch and all the other people trying to script gigantic, extreme-value C++ codebases use it. Try `pybind11` sometime, it's night and day.

Edit to reply to edit: `pybind11` supports an absurd amount of C++ out of the box with completely natural semantics and a very modest performance penalty. So, Python.

I wouldn't say python takes C++ interop seriously, it's more like the pybind11 people are amazing at what they do and found a way to slice the problem neatly. But yes, it's night and day, pybind11 is a godsend.
Haha I don't know if it matters much whether we praise the `pybind11` folks in particular or the Python community in general or both: they are fucking amazing at what they do. It's such a hard problem that most language communities don't even seriously try, it's a nightmare to get even close on, and they get more than close. Everything just works exactly as you'd expect, with great performance (relatively speaking of course) into the bargain.

Top ten best open source language efforts on Earth. Maybe top five. Just legendary stuff.

I do think that for all its faults though (cough packaging cough), the Python community displays an incredible commitment to getting shit done and helping people solve their problems. It's really a rather mediocre language as these scripty things go, but it just friggin works, which is why everyone uses it for everything.

Thanks, I'll look at pybind11. Have you looked at cxx by any chance?
I have. `cxx` and `autocxx` and `bindgen` and `cbindgen` are, better than nothing I guess? But they're all flakey and have weird corner cases (and crash sometimes! I'm looking at you `cbindgen`!) and don't handle containers well if at all and just, ugh.

I always end up saying fuck it and `extern "C"`-ing everything. It would be completely possible to make these tools work well, but the Rust ethos is "rewrite everything, pure Rust", at least in large parts of the community, and so these projects kind of never get totally dialed in.

It's not just C++ interop. We chose Zig because writing C library bindings for Rust is bewildering.
Yeah.

I know the talking points (it's impossible to go on the Internet and not know all the Rust talking points) but you have a few beers with some Rust people and pretty quickly you're hearing how anything that isn't Rust all the way down is somehow like, tainted, with the dreaded binary quality of being "unsafe" as opposed to "safe". A serious Rust hacker who shall remain nameless once broke out that old chestnut on me: "A barrel of wine and a spoonful of sewage makes a barrel of sewage." in reference to Rust software that links to C. It's not a coincidence that somewhere, every conceivable library that anyone could ever want is being rewritten in "pure Rust".

And it's a shame because Rust is fucking cool and I want to be using it even more than I already am. But I'm not going to become a Scientologist to get into a party in LA and I'm not going to throw away a mountain of excellent C and C++ because it's, unclean.

I'm optimistic that as Rust continues to have its center of gravity migrate away from strictly OSS and into ever higher stakes industrial settings (which it's clearly making great headway on) that the religious fervor will mellow and it'll become a "getting shit done" language that also happens to be a really cool language!

the first thing i did in rust was c lib bindings and it is simple.
The C library we need has a few requirements that most people agree are difficult for Rust.

Working with any of the following is a nightmare:

- Memory arenas

- Intrusive data structures

- Buffer lifetimes with interesting lifetimes (typically because it maps to hardware)

Given the vast C++ ecosystem (including pretty much every big game engine), there are still a ton of very good reasons to start a new big C++ project.

At least until Carbon is ready! But that's a few years away at the least.

> Unless clang-tidy has false positives, in which case the comparison isn't apples to apples then.

Confused... so you're suggesting Rust's checks are somehow free of false positives? Doesn't the halting problem get in the way? One Rust-specific example: https://www.reddit.com/r/rust/comments/nr7a33/is_the_borrow_...

There's a difference between code that used to compile no longer compiling because of an incorrect lint, and code that was never accepted. Rust is restrictive and gets less so over time. C and C++ need to become more restrictive over time, but that's a more traumatic direction.
> Rust is restrictive and gets less so over time. C and C++ need to become more restrictive over time, but that's a more traumatic direction.

I don't agree at all with your comment, and I find that sort of opinion miopic and not grounded on real-world software projects.

I 've worked on a fair share of legacy projects which were ported to the latest and greatest projects, including a couple of nightmare JavaScript ones. The very first thing we did was onboard onto static code analysis tools and source code formatters.

Once we enabled them we were faced with a big wall of red text dumps. With time that wall shrinked until there was no more red, and from thereon things stuck that way.

There was no trauma, only a kaizen approach to errors being flagged.

The whole C++ world already does this for decades, whether it's for compiler warnings, static code analysis, memory checkers, fuzzers, etc. What exactly leads you to believe this is traumatic?

What you're actually arguing seems to be "why I like Rust more than C++", not arguing why "clang-tidy has false positives, and thus the comparison isn't apples to apples then".

Clearly the positives can be just as false in Rust as in C++. Your actual objection is that anyone arguing that any feature of Clang can measure up to the corresponding feature of Rust at present is automatically disqualified from making that argument because... Clang's past "taints" its present? Like an original sin of sorts, but in programming? ("Apple" forbidden against comparison?)

I don't think it's about being tainted. It's that there's a bunch of C++ code out there already in Production in a ton of places that doesn't pass those checks, and may or may not actually be safe. It would be a ton of work that may not produce any end-user value for any C++ project to switch over to that.

Meanwhile, Rust has always had those checks, so there can't be any Rust code in Production that doesn't pass them that would be painful to switch over.

It's not about an original sin, it's about the practicalities of changing the default behaviour without pissing off your users.
Sure, but again, that's an argument for why you dislike C++, not an argument for why false positives in that Clang-Tidy check somehow disqualify it from being compared to the corresponding checks in Rust, especially when they both have false positives.
I find it so weird that the Rust community is borderline evangelical about memory safety when a) it's not actually memory safe once you start doing heavy shit b) modern C++ is quite memory safe and c) there are so many other great reasons to like Rust.

Memory safety in serious systems software is something that you approach asymptotically and/or probabilistically. Rust makes it easier to be memory safe in a lot of scenarios, at the cost of the father-knows-best borrow checker, but a crashed program is a crashed program whether I dereferenced a null pointer or was poking around in a slice with multi-byte Unicode characters in it. And that's before you get to `rg unsafe` on your favorite industrial-strength Rust codebase.

Rust is cool for so many great reasons that get talked about so little because everyone seems too busy acting superior about memory safety. Talk about traits! Or Cargo! Or the cool async stuff! Anything but another lecture on memory safety.

> a crashed program is a crashed program whether I dereferenced a null pointer or was poking around in a slice with multi-byte Unicode characters in it

They aren't the same thing though, that's the point.

Dereferencing a NULL pointer isn't guaranteed to crash. In fact if you are writing through the pointer, you may even have a security issue on your hands (rce, etc).

Safe Rust may have runtime errors that "crash" the program but this is a controlled, well-defined termination, and there is no way for the execution state itself to be corrupted like in C++.

Yep, my favorite part of heap/stack corruption is not when it crashes immediately but rather when it rears its head 2-3 weeks/months later when some upstream call pattern or timing has changed.

I've spend weeks chasing down single instances of this on multiple projects. The nasty part is you have no predictability in if it's going to be one that crashes immediately, silently writes garbage(hopefully not to disk!), is a latent lurking crash or security vuln.

If you trash the stack then there's a good chance you lose the backtrace as well which can make a hard to debug issue become "find the needle in the haystack". I hope it's something that reproduces quickly and consistently because otherwise you're in for a ride.

Yeah, this just isn't how it is anymore. The last time I was up shit creek because 50k boxes were crash looping and GDB couldn't get me a stack trace was in 2014. The last time I spent more than 30 minutes chasing a memory corruption issue was in like, 2018. And it was because some wise ass had decided to roll his own fibers by stomping on `rip`, `rbp`, and `rsp`.

These days you use `std::unique_ptr`, build with clang-tidy, CI under ASAN, and it's never an issue in practice. Once in a blue moon the CI chirps an ASAN failure that gives you the entire history of the memory address with line numbers and you fix the typo.

The safety that Rust gives me is that it's more expressive type system and modern affordances for things like exhaustive pattern matching lets me avoid logic errors, which are every bit as deadly as buffer overruns and much harder to mechanically identify.

It is usually easier to write correct code in Rust than in C++ because it's much more modern and frankly kind of an everyman's Haskell (which I mean as a compliment). But it's intellectually dishonest to say that this doesn't come at a cost: when you wander out of the borrow checker's sweet spot it can become kind of a Tetris puzzle even when you know all the rules on paper.

The same pattern matching that lets people see a borrow checker puzzle and immediately say "right, we need to do X" is the pattern matching that let's a C++ hacker see a failed template instantiation and immediately know what got misspelled.

It has been years since I spent any time chasing down memory usage errors.

I recommend compiling with warnings turned on, and acting on them.

> a) it's not actually memory safe once you start doing heavy shit b) modern C++ is quite memory safe

This just doesn't capture the problem that memory safety solves. A crashed program is not the worst-case scenario that it's trying to avoid. Even the most memory-safe language supports exiting early with an error message, or whatever.

In terms of language semantics, there is an all-or-nothing line between memory safety and undefined behavior. A memory safe program does what it says, locally, step-by-step, according to the semantics of the language. When a program exhibits UB, those guarantees are lost.

Of course, as you note, unsafe Rust also lets you violate memory safety, and in fact any memory safe language is at the mercy of its implementation and host. The reason people get evangelical about Rust's memory safety is one level higher: it offers a bridge back to memory safety, such that unsafe code stands on the same footing as the core language. When either are bug-free, the compiler can ensure they are used correctly, using the same type system features for both.

Modern C++ is certainly much less error-prone than the bad old days of manual `new` and `delete`, but it doesn't have an answer to this "unsafe encapsulation." To the contrary, modern C++ actually adds a bunch of new ways to violate memory safety by misusing library APIs. Iterator invalidation, use-after-move, string_view and span and borrowed ranges, by-reference lambda and coroutine captures, etc.

This all means that "serious systems software" in C++ has to approach memory safety via defensive copying or refcounting, copious use of sanitizers, and sandboxed sub-processes. Meanwhile, Rust programs can do things that would be unthinkable in a large C++ codebase, because the assumptions of both the language and unsafe code are encoded in the type system. (For example: https://manishearth.github.io/blog/2015/05/03/where-rust-rea...) It's a qualitatively different solution to the problem.

I think memory safety is the killer feature of rust, and has become so because people see the real world problem it's solving, more than through evangelicalism. We'll see in a few years when more "heavy shit" has been written/rewritten in rust. My prediction is that they will have significantly fewer memory safety issues than comparable c++ "heavy shit".
Business also needs to care,

> Many years later we asked our customers whether they wished us to provide an option to switch off these checks in the interests of efficiency on production runs. Unanimously, they urged us not to--they already knew how frequently subscript errors occur on production runs where failure to detect them could be disastrous. I note with fear and horror that even in 1980, language designers and users have not learned this lesson. In any respectable branch of engineering, failure to observe such elementary precautions would have long been against the law.

-- C.A.R Hoare on his Turing award speech in 1981.

From where I sit the killer feature of Rust is that a bunch of amazingly cool software is written in it, especially in the terminal. I'm a big terminal guy, and I can't think off the top of my head of anything I use constantly that isn't written in Rust. `rg`, `fzf`, `zoxide`, `bat`, `viddy`, the list goes on and on, I fucking love the shit people are writing in Rust.

And I think that should be the killer feature of a language: that cool software is written in it and is continuing to be written in it. This is a killer feature shared by Rust and C++ and these days to be serious about performant software in diverse settings, you pretty much have to know both well.

Terminal programs are one area where Rust's strengths seem to align (very good CLI libraries/parsing, error management, and concurrency) and weaknesses are less relevant (async, GUIs), which might be why it seems to be gaining traction in that area.
I'm embarrassed enough about misattributing like 2/5 to Rust that are written in golang, so I just dumped my minimum (non-work) `home.nix` package list: https://gist.github.com/johnnystackone/bacd9275296f3d5d0cd75....

I'm not going to go through every one and remember/look up which ones are written in Rust, but I'll wager that half-ish of them are.

Thanks for that list. I'd heard of rg and fzf but not the others.

I immediately thought: well what about Go for command line tools? Is this the viddy you speak of? https://github.com/sachaos/viddy If so, looks like it is written in Go. Looks like fzf too.

btw, fzf is written in go ^^
> We'll see in a few years when more "heavy shit" has been written/rewritten in rust.

I'd really like to see that. Would be cool to see a completely new Linux user space written in Rust. Not necessarily a rewrite of existing software, new ideas would be great. I tested Linux system calls and they worked very well even though they needed experimental inline assembly functionality to work. With system call support, anything is possible.

Once upon a time there was a project to do a Linux distribution in Ada.

Unfortunately it died a couple of years later.

Slowly there's more and more implementations emerging of Linux userland utils in Rust.

It's taking a while.

>a crashed program is a crashed program whether I dereferenced a null pointer or was poking around in a slice with multi-byte Unicode characters in it

Most of the biggest advances in software engineering are because of increased modularity. One of the best traditional ways to increase modularity is the ability to define and call functions. But any isolation between these "function" modules is only possible if you can at least factor out things into a function mechanically without introducing crashes (for example because of memory unsafety--modularity would fly out of the window right there).

>Rust is cool for so many great reasons that get talked about so little because everyone seems too busy acting superior about memory safety. Talk about traits! Or Cargo! Or the cool async stuff! Anything but another lecture on memory safety.

It's better not to dilute the message. All these other things are nice-to-have gimmicks. But the memory safety is a game-changer. It does no good to advertise 230 features at the same time. No one will remember. Advertise the killer feature. And that's the lifetime stuff, which gives you memory AND THREAD safety.

Rust's thread safety only applies to the special case of those threads accessing in process data segments.

Rust's type system can do very little to help when those threads are accessing the same record on a database without transactions, OS IPC on shared memory, manipulating files without locks, handling hardware signals, handling duplicate RPC calls,...

Yeah but that ultimately requires an unsafe block, kind of true, except no one reads the code of all crates they depend on, and the direct dependencies might be safe in what concerns the direct consumers.

This is a point that you constantly bring up in these threads, do you think most developers believe that data race safety should extend beyond the bounds of the process?

One thing that Rust’s type system does allow you to do is define a consistent manner in which to access external systems, even add types that will mimic the same safety. Is it perfect? Will it protect you from a different process working against the DB? Will it enforce things in the other process? No. But will it give you higher level semantics to be able to construct a better model for operating against that external system? Yes.

This has been a recurring theme from you, but in the cases you're describing the risk is only a race condition (no general solution is possible) and not a data race (which safe Rust is able to deny by design). These are categorically different problems.
Rust isn't a startup business or (one hopes) a religion, or an MLM, or a home for sale. It's a useful tool among many.

Why do people say things like: "It's better not to dilute the message"? Better for who? That's sales/marketing language, not engineering language.

"The message"? Pardon my Francais, but WTF?

>Why do people say things like: "It's better not to dilute the message"?

>Better for who?

Better for everyone.

When talking about a new thing, it would be really silly to emphasize how nice the logo is, how nice the package it comes in is, look at the awesome tape the box is closed with etc. If I turn the product off it even turns off! Look at the nice rounded corners of the device!

It even can do async! Just like Javascript and .NET.

Who cares!

What is the main strength of the tool, the pain point it was made to eliminate? Lead with that.

> That's sales/marketing language, not engineering language.

Leading with the actual technical novelty that actually advances the state of the art in production compilers is marketing? Well, I guess it's good marketing in a way.

The user will find cargo on their own in 5 minutes.

rustc rejects the resulting program if you mechanically factor out a function accessing &mut self, into a function holding mutable borrows to half the fields calling another function which access the other half of fields (or vice versa, the caller holding &field calling a method mutating other fields). This requires the more complex transformation of passing individual fields into the subfunction (more work, but sometimes easier to read), or waiting for Rust to add partial borrows. Note I've never actually hit this case myself, though I've heard it's an issue people run into.
The third option is to use a wrapper type with internal mutability, but that is to be done very sparingly.
Rust is "cool", lately. But it lacks basic features that enable capturing important semantics in libraries.

So the tradeoff is not relative safety against a little compile-time inconvenience. The tradeoff is against "no, you cannot code that thing at all, suck it".

So almost all discussion of relative safety (which Rust advocates would like us to think is absolute) carefully sidesteps the point that there is a very great deal that cannot be expressed in Rust at all -- and not because expressing those things would have been at all unsafe.

i honestly don't think i've seen "cool async stuff" as a substring before.
> Your point is a fair one, but defaults matter.

This sort of comment is far from fair and misses the whole point.

The thesis of this article is how programming languages compare with regards to safety.

It makes no sense at all to compare particular implementations and try to pass personal assertions on particular features of said implementations as broad assertions about the programming languages they support.

So a particular Rust implementation is bundled with a linter. That's pretty convenient.

It just so happens that the linter is provided by a compiler stack that is also one of the main reference implementations of C++, and said linter also supports C++.

Is it fair to thus claim Rust is somehow superior to C++ just because a particular implementation enables by default it's linter on Rust code but not C++?

> There's little reason for clang-tidy not to be part of the default clang invocation by now

It's perfectly fine that anyone forms their own personal opinion on what defaults a tool should ship with.

That says anything nothing about the programming languages and their safety though.

As is clear from my other comments I also find this stuff unfair.

I try to keep in mind that on balance it’s a good thing that performant programs have become dramatically more accessible recently, and that most of the C/C++/Fortran/Haskell antagonism is a result of enthusiasm around that. For me it was BASIC -> C, but I imagine JS -> Rust is every bit as exhilarating.

But I do hope at least a few people read your comment and are inspired to learn a little background. Rust is cool because it remixed the broadly-accessible FP algebra, a kickass C++ toolchain decades in the making, and a big bet on linear typing.

I didn’t set out to be the jaded, un-hip old guy but here we are :) It’s a nifty new LLVM front end with type classes and the Either monad. Neat. Get off my lawn ;)

They do, that is why any C++ shop where code quality is relevant has a DevOps team that cares about the right defaults being enforced on th CI/CD pipeline.

Devs that don't care only get to build on their own computers.

It isn't fullproof, but definitely helps to tame some cowboys.

To say the rust has a built in linter is wrong. A rust program that does not build because of a memory ownership error on the part of the programmer isn't rejected by the compiler due to a detected pattern, it is rejected because the program is "unsolvable" and cannot be built. I think a lot of people miss how integral the memory semantics Ruct enforces are to how it parses and compiles programs. If these things were simply lints it could be reasoned that a program could be built without following these semantics, that If you could simply go into the compilers source you could just turn them off/remove them. You can't. The way rust's compiler tracks memory is fundamental to how it compiles the binary. It is not simply pattern matching code or ast. Rust's compiler is actually tracking the lifecycle of every bit of memory allocated so it knows when to free it, and it does this at compile time without running the program. These memory semantics errors exist because they are integral. Turning them off would simply result in a broken compiler, or a program with no freeing of memory because the compile time reference counting rust implements becomes impossible.
Objectively speaking, Rust does not "have a lot of great qualities that C++ lacks". Any new feature or improvement has its pluses, but also its minuses.

* Rust has traits, but does not support OOP. Architectures where OOP is particularly effective are proving to be a significant challenge for Rust - GUIs are the obvious one, but also game development Rust projects have to invent new approaches.

* Option/Result make the code flow obvious, but having to return them in nearly all function calls is tedious. Rust has had several attempts at alleviating this problem, but still hasn't matched the convenience of exceptions.

* match is IMO syntactic sugar and its benefits for correctness are being oversold. The fact that one is basically obliged to use it leads to code that's sometimes too deep. Exceptions would cut through this error-handling noise, if they were available.

* The build story is convenient, but this had the unintended effect of encouraging dependency explosion. Adding typical crates results in dozens of transitive dependencies being included in a project.

The notable improvement that Rust brings to the table is machine-verified memory safety with C++-like performance, but that of course comes at the cost of having to adapt code to what the borrow checker understands. If one needs the feature, then the cost is worth paying, if not, Rust is more of a personal choice than an inevitable conclusion.

And finally, perhaps Rust's biggest sin is that it's big, it's complex and there's no end in sight to the complexity spiral, just like for C++. I can only imagine the chagrin of the Rust community as they see themselves competing with Go for many projects where performance is not absolutely critical and often being second choice exactly because of this complexity.

More to the point, Rust lacks key features I need to capture essential semantics into libraries. So, the libraries I could write in Rust would be less powerful than libraries I can write in C++.

Among common uses for these more powerful features is to make misuse of the library into a compile-time error. Coding the library in Rust, if possible at all, would mean failing to prevent these usage errors.

The point here is that C++ puts more power in the hands of a library writer, and both the responsibility and capability to enforce safety in uses of the library. Rust jealously reserves maintaining safety to the compiler alone.

Could you elaborate on this?
And way, way better compiler errors thanks to a sane generics system
And much better compiler errors. `rustc` about leads the pack on error messages of all the languages I use regularly.
I think you meant thanks to a sane(r) macro system? Both Rust and C++ use monomorphisation for generics, I believe shitty compiler errors are due to C++'s templating.
I’m what way can you have ‘generics’ in C++ that are not based on templating? I am almost certain that any implementation of anything ‘generic’ templates are inherently involved. Maybe I’m wrong about what you mean by generics though.
There are concepts now which are close enough to Rust's traits.