| |
| ▲ | sunshowers 3 days ago | parent | next [-] | | > 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. | |
| ▲ | tialaramex 3 days ago | parent | prev | next [-] | | 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? | | |
| ▲ | jcranmer 3 days ago | parent | next [-] | | > A retained mode GUI looks like a hierarchy to me, how do you say it isn't? The problem with retained mode GUIs is that, while the widget tree is, well, a a nice hierarchical tree, the widgets and the data model often need mutable references to each other, and it's these cross-references that break the tree-based ownership model of Rust. | |
| ▲ | 3 days ago | parent | prev | next [-] | | [deleted] | |
| ▲ | saghm 3 days ago | parent | prev | next [-] | | > Things that live forever can be immutably borrowed, no problem And mutably too, honestly, if that's what you need: https://doc.rust-lang.org/std/boxed/struct.Box.html#method.l... | |
| ▲ | TuxSH 3 days ago | parent | prev [-] | | > 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. This corresponds to a C++ feature that has been added in C++11 (and other languages have this too AFAIK); good for libraries but but this has a runtime performance cost. Since C++20 you can use constinit, or std::construct_at to avoid static initialization woes entirely > 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. True about C, and also true of most userspace applications. However, the whole point of intrusive list (and other intrusive structures) is to flip the ownership relationship - the object owns the node, instead of the node owning the object. In other words, the intrusive list is a non-owning view over its elements. More importantly, they allow the same object to be present in multiple lists at once (as long as you add enough node hooks). For example, you can have a pool-allocated Thread object that is simultaneously part of a queue of inactive threads of priority N, and also part of a wait queue for something else. > A retained mode GUI looks like a hierarchy to me, how do you say it isn't? Right, I conflated it with the other examples, my bad; there it's because of shared mutable state, children elements needing to update their parents, and lack of inheritance in Rust | | |
| ▲ | tialaramex 3 days ago | parent [-] | | > Since C++20 you can use constinit, or std::construct_at to avoid static initialization woes entirely This is just yet another bad defaults situation. Yes, that's the correct behaviour and so it's what Rust's statics do, but it wasn't how C++ worked so they had to add this constinit keyword. LazyStatic is solving the other thing - where we don't need runtime parameters but we are doing work at runtime so we aren't truly constant. |
|
| |
| ▲ | IshKebab 3 days ago | parent | prev | next [-] | | 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. | |
| ▲ | safercplusplus 3 days ago | parent | prev | next [-] | | > 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++. | | |
| ▲ | Animats 3 days ago | parent [-] | | > 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. Yes. Back references are a big problem. I just wrote a bidirectional transitive closure algorithm that uses many back references, with heavy use of Rc, RefCell, Weak, and ".borrow()". It's 100% safe rust.
This is the "proper" Rust way to write this sort of thing. The nice thing about doing it the "right" way was that, once it compiled, it needed few changes to work correctly. No mysterious errors at all.
But it took a lot of work to get it to compile. Some sections had to be rewritten to get the ownership plumbing right. I put it up on the Rust forums for comments, and got replies that I should stop doing all that fancy stuff and just use indices into arrays.[1] Or arena allocation. Things that bypass the Rust ownership system. Those approaches would probably have more bugs. (I'm starting to see a way to do compile time checking for this sort of thing. The basic concept is that run time borrows must be disjoint as to type, disjoint as to scope, or disjoint as to instance. The first is easy. The second requires inspecting the call chain, and there are problems with templates due to ambiguity over what a type parameter does in .borrow() activity. The third is almost a theorem proving problem, but if you restrict compile time checks for disjoint instances to a single function (or maybe a "class", a struct and its functions), it might be manageable. All this might take too much cleverness to use in practice. Too much time getting the ownership plumbing right, even with compiler support.
But I should write this up.) [1] https://users.rust-lang.org/t/bidrectional-transitive-closur... | | |
| ▲ | Seattle3503 3 days ago | parent [-] | | > I put it up on the Rust forums for comments, and got replies that I should stop doing all that fancy stuff and just use indices into arrays.[1] Or arena allocation. Things that bypass the Rust ownership system. Those approaches would probably have more bugs. I ran into this years ago as well. It was very unsatisfying. Maybe Rust is just missing a good GC type? | | |
| ▲ | Animats 2 days ago | parent [-] | | It's not an allocation problem. It's a back-reference problem. When struct A owns struct B, and B needs to be able to find A, that's surprisingly difficult to set up in Rust. Even for structures where B is inside of A's struct. |
|
|
| |
| ▲ | throwawaymaths 3 days ago | parent | prev [-] | | k8s was originally java, iirc | | |
| ▲ | pjmlp 3 days ago | parent [-] | | It was, then a couple of early Go advocates joined the team, and .... |
|
|
| |
| ▲ | fpoling 3 days ago | parent [-] | | Go jobs are not that popular either especially outside US. Java is way more popular and with recent improvements on JDK Go does not offer that match advantages over it. And compared with Go Java is fully memory-safe and have independent implementations. | | |
| ▲ | speed_spread 3 days ago | parent | next [-] | | I like modern Java and agree it's a much better language than Go and an excellent platform in general. But Java will not beat Go where small and fast native executables are required. The native story for Java will remain a sad one, builds being hyper slow (aot) or resulting in huge binaries (jlink). | | |
| ▲ | lenkite 3 days ago | parent | next [-] | | Beyond a certain project size, GraalVM compiles to the same binary size as Go. Many k8s projects produce binary sizes of >100MB. I think this is because LOC in Go scales poorly. Go's real edge over Java is in memory consumption, not in performance or binary size. Yet, this is also mostly true at the small-scale level. The edge again disappears for large projects needing large heaps. | |
| ▲ | fpoling 3 days ago | parent | prev | next [-] | | If one needs a small and fast executable, then Rust should be used, not Go. I know a company that wished they would use Rust, not Go, for their services as poor Go performance and GC spikes made it a big headache. | |
| ▲ | krior 3 days ago | parent | prev | next [-] | | Small and fast native executables sounds more like a niche than a real market. Don't get me wrong: really nice to have, but a raison d'etre? | |
| ▲ | pjmlp 3 days ago | parent | prev [-] | | You mean like those 300MB that need to be compressed with upx for distribution, like some Go projects? |
| |
| ▲ | melodyogonna 3 days ago | parent | prev [-] | | Then you've not tried to do any cloud-native development. Go in cloud is like Python in AI | | |
| ▲ | pjmlp 3 days ago | parent [-] | | I have, and Go over here is only relevant as the language Docker and Kubernetes are written on. Everything we do is written in JVM or CLR languages, JS/TS, and the occasional C++ for native libraries. | | |
| ▲ | melodyogonna 3 days ago | parent [-] | | You must be joking. Over 75% of software on CNCF is written in Go; these include tools ranging from CI/CD to metrics and monitoring. It may not be popular to write your enterprise applications but it is certainly very popular in cloud infrastructure | | |
| ▲ | pjmlp 3 days ago | parent [-] | | Quite serious, CNCF products are basically Docker and Kubernetes related, that isn't what actual companies care about when moving their compute workloads from in-house into AWS, Azure, GCP, Vercel, Nelify, while paying big boys money to consulting agencies. Many of those products are startups looking for attention in podcasts interviews and the like, and barely part of any enterprise cloud projects. |
|
|
|
|
|