logoalt Hacker News

TuxSHyesterday at 9:43 PM5 repliesview on HN

> Still, I see Rust as the natural progression from C++

I don't; Rust has its niche but currently can't replace C++ everywhere.

From what I'm aware of, Rust has poor ergonomics for programs that have non-hierarchical ownership model (ie. not representable by trees), for example retained mode GUIs, game engines, intrusive lists in general, non-owning pointers of subobjects part of the same forever-lived singleton, etc.

> Go

To displace Go you must also displace Kubernetes and its ecosystem (unlikely, k8s is such a convenient tool), or have k8s move away from Go (not gonna happen considering who developed both)


Replies

IshKebabtoday at 11:09 AM

I think people are still working out the best way to do GUIs and game engines in Rust, but I wouldn't say that's because it's impossible. It's just quite new.

It took decades to get a C++ GUI framework that was really nice (Qt) and even then it used MOC so wasn't pure C++.

Intrusive lists are quite niche and not a big deal.

I think people are working on improving the ergonomics for Rc<Refcell< etc.

So there are definitely some downsides, but also so many upsides. You could easily say C++ can't replace Rust in many areas because of X, Y, Z.

sunshowersyesterday at 10:57 PM

> intrusive lists in general

Not quite an intrusive list, but a particular data structure that's often called "intrusive" is a map where keys borrow from values. This turns out to be a super useful pattern.

https://www.boost.org/doc/libs/1_59_0/doc/html/boost/intrusi...

Note that the signature here is `const key_type & operator()(const value_type &)`, or in Rust terms `fn key(&self) -> &Self::Key`. This means that the key must exist in a concrete form inside the value -- it is impossible to have keys that borrow from multiple fields within the value, for example.

At Oxide, I recently wrote and released https://docs.rs/iddqd, which provides similar maps. With iddqd, the signature is `fn key(&self) -> Self::Key<'_>`, which means that iddqd does allow you to borrow from multiple fields to form the key. See the ArtifactKey example under https://docs.rs/iddqd/latest/iddqd/#examples.

tialaramexyesterday at 11:24 PM

Things that live forever can be immutably borrowed, no problem.

So rather than a non-owning pointer, you'd just use a &'static here - the immutable borrow which (potentially) lives forever

Years ago it was tricky to write the "forever, once initialized" type, you'd need a third party crate to do it stably. But today you can just use std::sync::LazyLock which lets you say OK, the first time somebody wants this thing we'll make it, and subsequently it just exists forever.

[If you need to specify some runtime parameters later, but still only initialize once and re-use you want OnceLock not LazyLock, the OnceLock is allowed to say there isn't a value yet]

Intrusive lists are one of those things where technically you might need them, but so often they're just because a C or C++ programmer knew how to write one. They're like the snorkel on the 4x4 I see parked in my street. I'd be surprised if they've ever driven it on a muddy field, much less anywhere the snorkel would help.

A retained mode GUI looks like a hierarchy to me, how do you say it isn't?

show 3 replies
safercplusplustoday at 12:50 AM

> From what I'm aware of, Rust has poor ergonomics for programs that have non-hierarchical ownership model (ie. not representable by trees)

Yeah, non-hierarchical references don't really lend themselves to static safety enforcement, so the question is what kind of run-time support the language has for non-hierarchical references. But here Rust has a disadvantage in that its moves are (necessarily) trivial and destructive.

For example, the scpptool-enforced memory-safe subset of C++ has non-owning smart pointers that safely support non-hierarchical (and even cyclical) referencing.

They work by wrapping the target object's type in a transparent wrapper that adds a destructor that informs any targeting smart pointers that the object is about to become invalid (or, optionally, any other action that can ensure memory safety). (You can avoid needing to wrap the target object's type by using a "proxy" object.)

Since they're non-owning, these smart pointers don't impose any restrictions on when/where/how they, or their target objects, are allocated, and can be used more-or-less as drop-in replacements for raw pointers.

Unfortunately, this technique can't be duplicated in Rust. One reason being that in Rust, if an object is moved, its original memory location becomes invalid without any destructor/drop function being called. So there's no opportunity to inform any targeting (smart) pointers of the invalidation. So, as you noted, the options in Rust are less optimal. (Not just "ergonomically", but in terms of performance, memory efficiency, and/or correctness checking.) And they're intrusive. They require that the target objects be allocated in certain ways.

Rust's policy of moves being (necessarily) trivial and destructive has some advantages, but it is not required (or arguably even helpful) for achieving "minimal-overhead" memory safety. And it comes with this significant cost in terms of non-hierarchical references.

So it seems to me that, at least in theory, an enforced memory-safe subset of C++, that does not add any requirements regarding moves being trivial or destructive, would be a more natural progression from traditional C++.

throwawaymathsyesterday at 10:22 PM

k8s was originally java, iirc