[HN Gopher] Understanding Memory Management, Part 5: Fighting wi...
       ___________________________________________________________________
        
       Understanding Memory Management, Part 5: Fighting with Rust
        
       Author : Curiositry
       Score  : 133 points
       Date   : 2025-05-03 21:01 UTC (3 days ago)
        
 (HTM) web link (educatedguesswork.org)
 (TXT) w3m dump (educatedguesswork.org)
        
       | sidcool wrote:
       | This is brilliantly written n
        
         | bnjms wrote:
         | Im sure it is. I don't know how to program but read most of
         | part 4 when it appeared here last.
        
       | Animats wrote:
       | This is pretty good.
       | 
       | A useful way to think about this:
       | 
       | - All data in Rust has exactly one owner.
       | 
       | - If you need some kind of multiple ownership, you have to make
       | the owner be a reference-counted cell, such as Rc or Arc.
       | 
       | - All data can be accessed by one reader/writer, or N readers,
       | but not both at the same time.
       | 
       | - There is both compile time and run time machinery to strictly
       | enforce this.
       | 
       | Once you get that, you can see what the borrow checker is trying
       | to do for you.
        
       | Surac wrote:
       | I really like the text. Giving more light to the memory
       | management of rust will help me understand more of the language.
       | I still think some concepts of rust are over verbose but I slowly
       | understand the hype around rust. I myself use C or C++ but I will
       | ,,borrow" some of the rust ideas to make my code even more robust
        
         | ultimaweapon wrote:
         | I'm coming from C++ now I don't want to use C++ anymore. When
         | C++ was still my primary language I always frustrated with some
         | of its feature like non-destructive move, copy by default and
         | dangling references then I found Rust fixed all of those
         | problems. At the beginning I very frustrated with Rust because
         | the borrow checker prevent me from doing what I usually do in
         | C++ but I keep going.
        
       | steveklabnik wrote:
       | This is a good post! A few comments:
       | 
       | > Function Overloads
       | 
       | Strictly speaking, Rust doesn't support overloaded functions.
       | Function overloading is when you define the same function but
       | with different arguments, and the language selects the correct
       | one based on the argument type. In this case, it's two different
       | implementations of a trait for two different types. That said,
       | it's close enough that this isn't really an issue, more of just a
       | technical note, since this is a series trying to get into
       | details.
       | 
       | > I can't find an explanation in the Rust documentation but I
       | expect the reason is that someone could implement another trait
       | that provides .into_iter() on whatever the x is in for y in x,
       | thus resulting in a compiler error because there would be two
       | candidate implementations.
       | 
       | Yep, I'm not sure that there is an official explanation anywhere
       | else, but this is exactly what I would assume as well. This
       | ensures that the correct implementation is called. This is also
       | one of the reasons why adding a trait implementation isn't
       | considered a breaking change, even if it could create a compiler
       | error, because you can always expand it yourself to explicitly
       | select the correct choice. Of course, these situations are
       | usually treated more carefully then they have to be, because
       | breakage isn't fun, even if it's technically allowed.
       | 
       | > But wait, you say, I'm doing exactly this in the first program,
       | and indeed you are.
       | 
       | It's not the same, as the next paragraphs explain.
       | 
       | > We are able to examine the function and realize it's safe, but
       | because the compiler wants to use local reasoning, it's not able
       | to do so.
       | 
       | This is a super important point!
        
         | junon wrote:
         | > I can't find an explanation in the Rust documentation but I
         | expect the reason is that someone could implement another trait
         | that provides .into_iter() on whatever the x is in for y in x,
         | thus resulting in a compiler error because there would be two
         | candidate implementations.
         | 
         | Because nothing in Rust is identifier-based. Unlike python, all
         | syntax magic (even the ? operator) relies on traits defined by
         | `core`.                   for x in y {
         | 
         | desugars to                   let mut iter =
         | IntoIterator::into_iter(y);         while let Some(x) =
         | y.next() {
         | 
         | and                   x?
         | 
         | desugars to                   match x {             Ok(x) => x,
         | Err(e) => {                 return Err(From::from(e));
         | }         }
         | 
         | and                   x + y
         | 
         | desugars to                   core::ops::Add::add(x, y)
         | 
         | etc.
         | 
         | All of those traits are expected to live explicitly in the core
         | crate at well known paths. Otherwise you'd be writing methods
         | with absolutely no idea how the language would interact with
         | it. And if you had a Set type implement `add`, it'd have to
         | accept exactly 2 arguments to be compatible with the language's
         | `add` or something equally as unergonomic.
         | 
         | It's traits all the way down! There'd be no explanation needed
         | because it'd be antithetical and contradictory to traits to
         | begin with. Once one understands how traits are intended to be
         | used, the explanation for why there aren't identifier based
         | resolution semantics becomes obvious.
        
           | saagarjha wrote:
           | Swift has overloading and its own version of traits. None of
           | what you said is actually required.
        
           | norman784 wrote:
           | Interesting, I didn't knew about the for loop and the ops
           | desugars to something in core, I though it was deal in the
           | compiler, but makes sense since you can implement the ops::*
           | traits for your custom types.
        
           | writebetterc wrote:
           | It's a bit annoying that there is a standard desugaring of
           | all of this, but there's no actual way of implementing this
           | sugaring yourself. I guess you could have a token macro (or
           | whatever they're called) at the top scope and do it yourself?
           | 
           | Anyway, my conclusion is that in order to learn Rust you
           | can't use any syntactic sugar the language provides, those
           | are 'expert features'. There's no way I'd be able to figure
           | out why this happens. Do rust-analyzer provide a way of
           | desugaring these constructs?
        
             | steveklabnik wrote:
             | > there's no actual way of implementing this sugaring
             | yourself.
             | 
             | This is the common case in languages with sugar.
             | 
             | Heck, you could consider all of Rust sugar for MIR...
             | 
             | > There's no way I'd be able to figure out why this
             | happens.
             | 
             | You don't generally need to, but most of this is
             | documented:
             | 
             | https://doc.rust-lang.org/stable/std/iter/index.html#for-
             | loo...
             | 
             | https://doc.rust-
             | lang.org/stable/std/ops/trait.Add.html#impl...
             | 
             | ?'s desugaring isn't documented because the desugaring is
             | subject to change.
        
           | tialaramex wrote:
           | Several of those de-sugarings are wrong. The book explains
           | how Rust's `for` is actually de-sugared, and that ? is a Try
           | operator, so unsurprisingly it de-sugars to a use of
           | Try::branch and that gets you a ControlFlow which is then
           | pattern matched.
           | 
           | Also, they don't need to live "at well known paths". Instead
           | they're marked as "langitems", with the magic attribute lang:
           | 
           | #[lang = "add_assign"] // Signifies that this trait is what
           | the compiler wanted to make the += operator work in the
           | language.
        
           | steveklabnik wrote:
           | Other than what other people said, the question is to _why_
           | that desugaring is chosen, not that it 's because of a
           | specific one. That is, why                 let mut iter =
           | IntoIterator::into_iter(y);
           | 
           | instead of                 let mut iter = y.into_iter();
           | 
           | That _could_ have been done, but it wasn 't.
           | 
           | > Otherwise you'd be writing methods with absolutely no idea
           | how the language would interact with it.
           | 
           | Right, this is what's guessed, because it makes the most
           | sense.
        
         | kibwen wrote:
         | Re: function overloading, Rust doesn't support overloaded
         | functions in the same manner as Java or C++, which is important
         | to emphasize to people coming from those languages.
         | 
         | However, if you then show those people that this is legal Rust:
         | let addr1 = Ipv4Addr::from([13, 12, 11, 10]);         let addr2
         | = Ipv4Addr::from(218893066);
         | 
         | ...that also runs the risk of confusing them, because this is
         | indistinguishable from function overloading, because it
         | basically is function overloading.
         | 
         | I'm not actually sure what to call this, because unlike Java-
         | style overloading, Rust isn't ad-hoc; everything it's doing is
         | fundamentally integrated into the type system. But unlike
         | strict parametric polymorphism (and like Java), both of the
         | above functions have totally distinct implementations.
        
           | zozbot234 wrote:
           | > I'm not actually sure what to call this
           | 
           | I'd call it "Making ad-hoc polymorphism less ad hoc".
        
             | demurgos wrote:
             | My understanding is that "ad-hoc" in this context means
             | that you are not defining a single generic implementation
             | but adding individual impls for specific types. There's no
             | fundamental difference between Java's overloading and
             | Rust's implicitly inferred trait implementations in my
             | opinion.
             | 
             | Rust's implementation is more orthogonal, so specifying
             | which impl you want explicitly does not require special
             | syntax. It's also based on traits so you'd have to use a
             | non-idiomatic style if you wanted to use overloading as
             | pervasively as in Java. But are those really such big
             | differences? See my reply to the original where I post an
             | example using Rust nightly that is very close to
             | overloading in other languages.
        
               | tavianator wrote:
               | They were referencing the title of a specific paper,
               | which invented type classes:
               | https://dl.acm.org/doi/10.1145/75277.75283
        
               | demurgos wrote:
               | Thank you, I did not know this paper and totally missed
               | the reference.
        
           | steveklabnik wrote:
           | For sure, that's why I don't think it's the biggest deal.
           | 
           | > I'm not actually sure what to call this,
           | 
           | Just regular old name resolution. But that's not super
           | satisfying either.
        
         | demurgos wrote:
         | > Strictly speaking, Rust doesn't support overloaded functions.
         | Function overloading is when you define the same function but
         | with different arguments, and the language selects the correct
         | one based on the argument type. In this case, it's two
         | different implementations of a trait for two different types.
         | 
         | You're right that there is no function overload in this
         | article, just some implicit derefs.
         | 
         | I would argue however that Rust has overloaded functions
         | (function resolution based on argument types) due to how it
         | handles trait resolution with implicit inference. Rust may not
         | have syntax sugar to easily define overloads and people
         | generally try to avoid them, but using argument-based dispatch
         | is extremely common. The most famous example is probably
         | `MyType::from(...)`, but any single-method generic trait using
         | the generics for the method arguments is equivalent to function
         | overloading. There are also other techniques. Using nightly
         | features you can get far enough so a consumer can use native
         | function call syntax.
         | 
         | Overload on nightly: https://play.rust-
         | lang.org/?version=nightly&mode=debug&editi...
         | 
         | Overload on stable: https://play.rust-
         | lang.org/?version=stable&mode=debug&editio...
         | 
         | The mechanism and syntax may be different from overloading in
         | C++ or Java, but as a user the result is the same and it causes
         | the same pain points.
        
           | steveklabnik wrote:
           | > The mechanism and syntax may be different from overloading
           | 
           | That's all I'm saying.
           | 
           | > as a user the result is the same
           | 
           | That's why I said I didn't think the inaccuracy is the worst
           | detail.
        
       | cornholio wrote:
       | A 20 page document on how to use basic variables, function calls
       | and methods. Except for the threading paragraph, which is hard in
       | any language, this is all complexity and refactoring pain that
       | Rust hoists onto every programmer every day, for relatively
       | modest benefits, somewhat improved performance and memory usage
       | vs the garbage collected/ref-counted version of the same code.
       | 
       | Essentially, you wouldn't and shouldn't make that tradeoff for
       | anything other than system programming.
        
         | baq wrote:
         | > this is all complexity and refactoring pain that Rust hoists
         | onto every programmer every day
         | 
         | This is what you should be doing when working with C/C++,
         | except there is no compiler to call you names there if you
         | don't.
         | 
         | If you're saying 'use a GC language unless requirements are
         | strict about it', yeah hard to disagree.
        
           | francasso wrote:
           | > This is what you should be doing when working with C/C++
           | 
           | I genuinely wonder if you actually have ever written c/c++,
           | there is plenty of code that is perfectly valid and safe
           | (mostly involving multiple pointers to mutable memory being
           | alive) that the borrow check cannot accept because it has to
           | draw a line to things it can prove are correct.
           | 
           | It's like saying that the only valid math is the one that an
           | automated theorem prover can prove, it's not even close to
           | being true.
        
             | baq wrote:
             | > I genuinely wonder if you actually have ever written
             | c/c++
             | 
             | I have; enough for one lifetime if you ask me. It was
             | hunting use after delete which made me stop.
             | 
             | I kinda agree with you, with the caveat that both can be
             | true. If you want to write safe-ish C++ you'll use
             | defensive containers from the start and watch iterators
             | like a hawk. You can also take a more cavalier approach and
             | live with the consequences (which might not happen and then
             | you win big).
             | 
             | Rust wants you to basically not use references unless your
             | data fits the one writer xor many readers model (painfully
             | including struct members, recursively) and the cavalier
             | approach is very strongly discouraged on the language
             | level. This forces you towards safety, but I also agree
             | with everyone else who say that isn't how computers
             | actually work. The impedance mismatch is an engineering
             | tradeoff to make.
        
         | bsaul wrote:
         | I see why you're saying that, and i almost entirely agree with
         | you. However, i would say that if all you're doing is glueing
         | calls to third party systems (like what most backend code is),
         | then you won't fall into complex lifetime problems anyway, and
         | the experience will remain quite pleasant.
         | 
         | Another point, is that the rust ecosystem is absolutely
         | insanely good ( i've recently worked with uniffi and
         | wasmbindgen, and those are 5 years ahead of anything else i've
         | seen..)
        
       | bsaul wrote:
       | "If we change .set_value() to take a &self instead of a &self"
       | 
       | guess it's "instead of a &mut self"
        
       | diath wrote:
       | The first code snippet, which is as simple as it gets, perfectly
       | illustrates why Rust is extremely annoying to work with. I
       | understand why you need the into_iter bit and why the borrow
       | checker complains about it, but the fact that even the simplest
       | "for x in y" loop already makes you wrestle the compiler is just
       | poor ergonomics.
        
         | baq wrote:
         | > wrestle the compiler
         | 
         | This is quite literally a skill issue, no offense.
         | 
         | 'wrestle the compiler' implies you think you know better; this
         | is usually _not_ the case and the compiler is here to tell you
         | about it. It 's annoying to be bad at tracking ownership and
         | guess what: most people _are_. The ones who aren 't have
         | decades of experience in C/C++ and employ much the same
         | techniques that Rust guides you towards. If you really know
         | better, there are ways to get around the compiler. They're
         | verbose and marked unsafe to 1) discourage you from doing that
         | and 2) warn others to be extra careful here.
         | 
         | If this is all unnecessary for you - and I want to underscore I
         | agree it should be in most software development work - stick to
         | GC languages. With some elbow grease they can be made as
         | performant as low level languages if you write the critical
         | parts in a way you'd have to do it in Rust and will be free to
         | write the rest in a way which doesn't require years of
         | experience tracking ownership manually. (Note it won't hurt to
         | be tracking ownership anyway, it's just much less of an issue
         | if you have to put a weakref somewhere once a couple years vs
         | be aware at all times of what owns what.)
        
           | 59nadir wrote:
           | No, GP can just _not use Rust_ , they don't have to use GC
           | languages to have something that makes sense and doesn't
           | force you to always have a debate with the compiler about
           | even simple things.
           | 
           | If they used Odin (or Zig) they could've looped through that
           | dynamic array no problem, in fact:                   package
           | example                  import "core:fmt"
           | main :: proc() {             xs: [dynamic]int
           | append(&xs, 1, 2)                  for x in xs {
           | fmt.println(x)             }
           | fmt.println(len(xs))         }
           | 
           | It _is_ ridiculous that Rust complains even about the simple
           | for loop and to say that this somehow comes down to  "Well,
           | everyone would do it this way if they cared about memory
           | safety" is just not really true or valuable input, it sounds
           | like what someone would say if their only systems programming
           | experience came from Rust and they post-rationalized
           | everything they've seen in Rust as being how you have to do
           | it.
           | 
           | My tips to people who maybe feel like Rust seems a bit
           | overwrought:
           | 
           | Look for something else, check out Odin or Zig, they've got
           | tons of ways of dealing with memory that simply sidestep
           | everything that Rust is about (because inherently Rust and
           | everything that uses RAII has a broken model of how resources
           | should be managed).
           | 
           | I learned Odin just by reading its _Overview_ page
           | (https://odin-lang.org/docs/overview/) and trying stuff out
           | (nowadays there are also good videos about Odin on YouTube),
           | then found myself productively writing code after a weekend.
           | Now I create 3D engines using just Odin (and we in fact use
           | only a subset of what is on that Overview page). Things _can_
           | be simple, straight forward and more about the thing you 're
           | solving than the language you're using.
        
             | mjburgess wrote:
             | Rust, in many ways, is a terrible first systems programming
             | language.
             | 
             | To program a system is to engage with how the real devices
             | of a computer work, and very little of their operation is
             | exposed via Rust or even _can be_ exposed. The space of all
             | possible valid /safe Rust programs is tiny compare to the
             | space of all useful machine behaviours.
             | 
             | The world of "safe Rust" is a very distorted image of the
             | real machine.
        
               | masklinn wrote:
               | > Rust, in many ways, is a terrible first systems
               | programming language.
               | 
               | Contrariwise, Rust in, in many way, an awesome first
               | systems programming language. Because it tells you and
               | forces you to consider all the issues upfront.
               | 
               | For instance in 59nadir's example, what if the vector is
               | a vector of heap-allocated objects, and the loop frees
               | them? In Rust this makes essentially no difference,
               | because at iteration you tell the compiler whether the
               | vector is borrowed or moved and the rest of the lifecycle
               | falls out of that regardless of what's _in_ the vector:
               | with a borrowing iteration, you simply could not free the
               | contents. The vector generally works and is used the same
               | whether its contents are copiable or not.
        
               | mjburgess wrote:
               | None of these "issues" are systems issues, they're memory
               | safety issues. If you think systems programming is about
               | memory saftey, then you're demonstrating the problem.
               | 
               | Eg., some drivers _cannot_ be memory safe, because memory
               | is arranged outside of the driver to be picked up  "at
               | the right time, in the right place" and so on.
               | 
               | Statically-provable memory saftey is, ironically, quite a
               | bad property to have for a systems programming language,
               | as it prevents actually controlling the devices of the
               | machine. This is, of course, why rust has "unsafe" and
               | why anything actually systems-level is going to have a
               | fair amount of it.
               | 
               | The operation of machine devices isnt memory safe --
               | memory saftey is a static property of a program's source
               | code, that prevents describing the full behaviour of
               | devices correctly.
        
               | 59nadir wrote:
               | > For instance in 59nadir's example, what if the vector
               | is a vector of heap-allocated objects, and the loop frees
               | them?
               | 
               | But the loop doesn't free them. This is trivial for us to
               | see and honestly shouldn't be difficult for Rust to
               | figure out either. Once you've adopted overwrought tools
               | they should be designed to handle these types of issues,
               | otherwise you're just shuffling an esoteric burden onto
               | the user in a shape that doesn't match the code that was
               | written.
               | 
               | With less complicated languages we take on the more
               | general burden of making sure things make sense (pinky-
               | promise, etc.) and that is one that we've signed up for,
               | so we take care in the places that have actually been
               | identified, but they need to be found manually; that's
               | the tradeoff. The argument I'm making is that Rust really
               | ought to be smarter about this, there is no real reason
               | it shouldn't be able to understand what the loop does and
               | treat the iteration portion accordingly, but it's
               | difficult to make overcomplicated things because they are
               | exactly that.
               | 
               | I doubt that most Rust users feel this lack of basic
               | introspection as to what is happening in the loop makes
               | sense once you actually ask them, and I'd bet money most
               | of them feel that Rust ought to understand the loop
               | (though in reading these posts I realize that there are
               | actual humans that don't seem to understand the issue as
               | well, when it's as simple as just reading the code in
               | front of them and actually taking into account what it
               | does).
        
               | Ygg2 wrote:
               | > But the loop doesn't free them.
               | 
               | What if it did free them in a function you don't directly
               | control?
        
               | jandrewrogers wrote:
               | A lot of idiomatic systems code is _intrinsically_ memory
               | unsafe. The hardware owns direct references to objects in
               | your address space and completely disregards the
               | ownership semantics of your programming language. It is
               | the same reason immediately destroying moved-from objects
               | can be problematic: it isn't sufficient to statically
               | verify that the code no longer references that memory.
               | Hardware can and sometimes does hold references to moved-
               | from objects such that deferred destruction is required
               | for correctness.
               | 
               | How is someone supposed to learn idiomatic systems
               | programming in a language that struggles to express basic
               | elements of systems programming? Having no GC is
               | necessary but not sufficient to be a usable systems
               | language but it feels like some in the Rust community are
               | tacitly defining it that way. Being a systems programmer
               | means being comfortable with handling ambiguous object
               | ownership and lifetimes. Some performance and scalability
               | engineering essentially requires this, regardless of the
               | language you use.
        
               | dmitrygr wrote:
               | > forces you to consider all the issues upfront.
               | 
               | Ever wonder why we do not train pilots in 737s as their
               | first planes? Plenty of complex issues do NOT, in fact,
               | need to be considered upfront.
        
               | haradion wrote:
               | YMMV, naturally, but I've found that some embedded
               | devices have really excellent hardware abstraction layers
               | in Rust that wrap the majority of the device's
               | functionality in an effectively zero-overhead layer.
               | Timers? GPIO? Serial protocols? Interrupts? It's all
               | there.
               | 
               | - https://docs.rs/atsamd-hal/
               | 
               | - https://docs.rs/rp2040-hal/
        
             | dwattttt wrote:
             | I dunno; I've never tried Zig before, and it wasn't hard to
             | check whether this kind of bug was easy to have:
             | const std = @import("std");              pub fn main()
             | !void {           var gpa:
             | std.heap.GeneralPurposeAllocator(.{})=.{};           const
             | alloc=gpa.allocator();                  var list = try
             | std.ArrayList(u8).initCapacity(alloc, 1);           const a
             | = try list.addOne();           a.* = 0;
             | std.debug.print("a={}\n", .{a.*});           const b = try
             | list.addOne();           b.* = 0;
             | std.debug.print("a={}\n", .{a.*});
             | std.debug.print("b={}\n", .{b.*});       }
             | a=0       Segmentation fault at address 0x7f9f7b240000
        
               | 59nadir wrote:
               | And why do you think that bug is relevant in the case of
               | a loop that prints the elements of a container? We can
               | all see and verify at a glance that the code is valid,
               | it's just not provably valid by the Rust compiler.
               | 
               | I feel like these posts trying to show possible memory
               | issues with re-allocated dynamic arrays are missing the
               | point: There is no code changing the underlying array,
               | there is no risk of any kind of use-after-free error.
               | This is exactly the kind of case where all of this
               | jumping through hoops shouldn't be needed.
        
               | dwattttt wrote:
               | > There is no code changing the underlying array, there
               | is no risk of any kind of use-after-free error.
               | 
               | There is none of this code, until there is.
        
               | 59nadir wrote:
               | Ok, so we've established that the loop can be verified as
               | not changing the container in any way, what makes you
               | believe this shouldn't be obvious to the Rust compiler?
               | 
               | When code that modifies the container is added, it should
               | be understood and then correctly errored about, I don't
               | get why this is such a crazy concept to people.
               | 
               | The point here is that you pay the cost for an error that
               | can't happen. It's just a micro example of a much more
               | general issue that boils down to:
               | 
               | The Rust compiler does a lot to find and mitigate bugs,
               | it's amazing, but it also rejects completely valid
               | programs and solutions because it simply isn't good
               | enough (and it's a difficult enough problem where I'm
               | prepared to say it will never be good enough). You can
               | either pay that cost constantly and for certain problems
               | be dealing with it (a lot) for no gain whatsoever
               | (because the bugs it was trying to prevent weren't actual
               | issues or are in fact imperatives because the thing
               | you're doing requires them) or you can choose not to.
               | 
               | I don't think it's particularly useful to make excuses
               | for the compiler not understanding very basic things in
               | simple examples and indirectly argue that it would be too
               | complicated to see what the loop is doing and act
               | accordingly. Rust already signed up for a very
               | complicated compiler that does all kinds of crazy things
               | in order to mitigate bugs; this type of introspection
               | would increase the accuracy of it a lot.
        
               | dwattttt wrote:
               | > Ok, so we've established that the loop can be verified
               | as not changing the container in any way, what makes you
               | believe this shouldn't be obvious to the Rust compiler?
               | 
               | I would be quite happy for the Rust compiler to be able
               | to perform more powerful analysis and make writing code
               | easier. What I object to, and I think that quite small
               | Zig code snippet highlights, is that dealing with those
               | shortcomings
               | 
               | > for no gain whatsoever
               | 
               | is also plainly wrong.
        
               | 59nadir wrote:
               | I make custom 3D engines and I can tell you that it would
               | not be a net benefit for us to use Rust. That's why I
               | added "for certain problems" as a qualifier; there are
               | use cases where Rust would be a net negative.
               | 
               | There are also plenty of use cases where Rust is actually
               | useful and provides guarantees about things that you want
               | guarantees about.
        
               | Ygg2 wrote:
               | > You can either pay that cost constantly and for certain
               | problems be dealing with it (a lot) for no gain
               | whatsoever (because the bugs it was trying to prevent
               | weren't actual issues or are in fact imperatives because
               | the thing you're doing requires them) or you can choose
               | not to.
               | 
               | Alternatively, you can use Rust so much these limitations
               | become second nature, and thus don't make them in the
               | first place.
               | 
               | > I don't think it's particularly useful to make excuses
               | for the compiler not understanding very basic things in
               | simple examples and indirectly argue that it would be too
               | complicated to see what the loop is doing and act
               | accordingly.
               | 
               | Great idea, until it stops working. It runs into the
               | paraphrased quote: "Any sufficiently complicated borrow
               | checker is indistinguishable from Dark Magic".
               | 
               | First you say, well, the compiler should be sufficiently
               | smart to figure out case A1 should work, then you add
               | that, but then arises another case A2 that the compiler
               | is sufficiently smart to figure out and so on.
               | 
               | However, you add a bunch of these "sufficiently smart"
               | borrow rules, and you'll end up with a mess. A1 and A2
               | don't work if A432 is applied, but do work if A49324 is
               | given if the A4 and A2 are satisfied.
               | 
               | The harder the borrow checker is to understand, the more
               | difficult it is to construct a mental model that's
               | useful.
               | 
               | In summary: while I'm not against improving the borrow
               | checker, but the problem is that it needs to be balanced
               | with the opportunity cost of understanding how it
               | approximately works.
        
               | genrilz wrote:
               | I think it is important to note that in 59nadir's
               | example, the reason Rust gives an error and Odin doesn't
               | is not memory safety. Rust uses move semantics by default
               | in a loop while Odin appears to use copy semantics by
               | default. I don't really know Odin, but it seems like it
               | is a language that doesn't have RAII. In which case, copy
               | semantics are fine for Odin, but in Rust they could
               | result in a lot of extra allocations if your vector was
               | holding RAII heap allocating objects. Obviously that
               | means you would need to be careful about how to use
               | pointers in Odin, but the choice of moving or copying by
               | default for a loop has nothing to do with this. For
               | reference:
               | 
               | Odin (from documentation):                 for x in
               | some_array { // copy semantics       for &x in some_array
               | { // reference semantics       // no move semantics?
               | (could be wrong on this)
               | 
               | Rust:                 for x in vec.iter_ref().copied() {
               | // bytewise copy semantics (only for POD types)       for
               | x in vec.iter_ref().cloned() { // RAII copy semantics
               | for x in &vec { // reference semantics       for x in vec
               | { // move semantics
               | 
               | C++:                 for (auto x : vec) { // copy
               | semantics       for (auto &x : vec) { // reference
               | semantics       for (auto &&x : vec) { // move semantics
        
             | tcfhgj wrote:
             | And how should resources be managed?
        
               | 59nadir wrote:
               | In bulk, i.e. not one-by-one as is implied and most used
               | with RAII. RAII works best for a one-by-one use case and
               | in well designed, performant systems the one-by-one use
               | case is either irrelevant, rare or an anti-pattern.
        
               | tcfhgj wrote:
               | if you want bulk, you can use arrays, vecs, arenas, etc.
        
             | klaussilveira wrote:
             | For anyone curious about Odin and graphics, it seems to
             | work really well:
             | 
             | https://gist.github.com/NotKyon/6dbd5e4234bce967f7350457c1e
             | 9...
             | 
             | https://www.youtube.com/watch?v=gp_ECHhEDiA
        
           | diath wrote:
           | This is yet another issue with Rust, nowhere in my post have
           | I mentioned C++, I made no effort comparing the two
           | languages, I just pointed out poor developer ergonomics in
           | Rust and you're instigating a language flame war as if you
           | took valid criticism as a personal attack; you can do better
           | than that.
        
             | ninkendo wrote:
             | My 2C/, it's perfectly reasonable to bring up other
             | languages in defense of criticism, because it explains why
             | these decisions were made in the first place. GP literally
             | said that rust isn't a good fit for you if you're in a
             | position to use a GC. The comparison to C++ is important
             | because it's one of very, very few contemporary languages
             | that also doesn't require a GC/refcounting everywhere. So
             | it's useful to compare to how C++ does it.
             | 
             | Yet another issue with _people who criticize rust_ : they
             | don't want anyone to defend rust, and complain loudly about
             | anyone defending rust as being a literal _problem with the
             | language_. You can do better than that.
        
             | Ygg2 wrote:
             | > poor developer ergonomics
             | 
             | I don't think it's poor developer ergonomics. The compiler
             | tells you "Hey, try adding &x at this position".
             | 
             | It's unfamiliarity with Rust's type system and syntax
             | sugars.
             | 
             | <HYPERBOLE>
             | 
             | I'll take 1000 compiler errors over a single non-
             | deterministic bug that only happens on ARM at 6 o'clock on
             | Blue Moon when Mercury is in Orion (it's UB).
             | 
             | And I'd ritually sacrifice my first unborn werekid to the
             | Dark Compiler Gods for a compiler error, that actually
             | suggests a correct fix.
             | 
             | </HYPERBOLE>
        
           | mjburgess wrote:
           | That systems languages have to establish (1) memory saftey,
           | (2) statically; (3) via a highly specific kind of type system
           | given in Rust; and (4) with limited inference -- suggests a
           | lack of imagination.
           | 
           | The space of all possible robust systems languages is vastly
           | larger than Rust.
           | 
           | It's specific choices force confronting the need to
           | statically prove memory saftey via a cumbersome type system
           | very early -- this is not a divine command upon language
           | design.
        
             | kobebrookskC3 wrote:
             | sure, rust is not the final answer to eliminating memory
             | safety bugs from systems programming. but what are the
             | alternatives, that aren't even more onerous and/or limited
             | in scope (ats, frama-c, proofs)?
        
               | mjburgess wrote:
               | My preference is to have better dynamic models of devices
               | (eg., how many memory cells does ram have, how do these
               | work dynamically, etc.) and line them up with well-
               | defined input/output boundaries of programmes. Kinda
               | "better fuzzing".
               | 
               | I mean, can we run a program in a well-defined "debugging
               | operating system" in a VM, with simulated "debugging
               | devices" and so on?
               | 
               | I dont know much about that idea, and the degree to which
               | that vision is viable. However it's increasingly how the
               | most robust software is tested -- by "massive-scale
               | simulation". My guess is it isnt a major part of, say,
               | academic study because its building tools over years
               | rather than writing one-off papers over months.
               | 
               | However, if we had this "debuggable device environment",
               | i'd say it'd be vastly more powerful than Rust's static
               | guarantees and allow for a kind of "fearless systems
               | programming" without each loop becoming a sudoku puzzle.
        
             | Ygg2 wrote:
             | > The space of all possible robust systems languages is
             | vastly larger than Rust.
             | 
             | The space of all possible CVE is also vastly larger outside
             | of Rust as well.
             | 
             | My biggest takeaway from Rust isn't that it's better C++.
             | But that it's extremely fast (no runtime limited GC) and
             | less footgunny Java.
        
           | shakna wrote:
           | > It's annoying to be bad at tracking ownership and guess
           | what: most people are. The ones who aren't have decades of
           | experience in C/C++ and employ much the same techniques that
           | Rust guides you towards.
           | 
           | You wouldn't need to do that here, in SPARK, or Oberon, or
           | just about any other memory safe language. This is unique to
           | Rust, and their model - and it absolutely is not required for
           | safety. It's not a skill issue. It's a language design
           | problem.
        
             | kobebrookskC3 wrote:
             | doesn't spark do something inspired by rust to get safe
             | dynamic memory allocation? https://docs.adacore.com/spark20
             | 14-docs/html/ug/en/source/ac...
             | 
             | what does oberon do?
        
               | shakna wrote:
               | SPARK has had it before Rust existed. However, gnatprove
               | doesn't require you the programmer to change anything.
               | The compiler does the work to ensure safety, not you.
               | 
               | Oberon is similar. The typesolver will determine if
               | something is safe, without the need for explicitly
               | borrowing anything.
        
           | pron wrote:
           | > 'wrestle the compiler' implies you think you know better;
           | this is usually not the case and the compiler is here to tell
           | you about it.
           | 
           | Well, yes and no. The way type systems work to soundly
           | guarantee that some program property P holds is by
           | guaranteeing some _stronger_ property Q, such that Q = > P.
           | This is because type systems generally enforce what we call
           | "inductive invariant", i.e. a property that is preserved by
           | all program statements [1], while most interesting program
           | properties are not inductive. To give an example, suppose
           | we're interested in the property that a program produces an
           | even number; an inductive invariant that implies that
           | property is one that makes sure that the outcome of _all_
           | computations in the language are even. A program that
           | satisfies the latter property obviously satisfies the former,
           | but the converse isn 't true.
           | 
           | Similarly, the way Rust guarantees that all programs don't
           | have, say, use-after-free, is by enforcing a _stronger_
           | property around ownership. So all safe Rust programs don 't
           | have use-after-free, but many programs that don't have use-
           | after-free don't satisfy the stronger ownership property.
           | This means that sometimes (and this is true for all sound
           | type systems) you have to "wrestle" the compiler, which
           | insists on the stronger property, even though you know that
           | the weaker property -- the one you're interested in -- holds.
           | In other words, sometimes you do know better than the
           | compiler.
           | 
           | That is not to say that the approach where the compiler
           | enforces stronger invariants is always right or always wrong,
           | or even right or wrong most of the time, but that "wrestling
           | the compiler" is something that even the most skilled
           | programmers confront from time to time.
           | 
           | [1]: This is because inductive invariants are compositional,
           | i.e. they hold for some composition of program terms t and s
           | iff they hold for t and s and their composition operator, and
           | type systems want to be compositional.
        
         | 0xdeafbeef wrote:
         | #include <vector> #include <iostream>                 int
         | main() {           std::vector<int> vec = {1, 2, 3};
         | vec.reserve(3);                std::cout << "Initial capacity:
         | " << vec.capacity() << std::endl;           std::cout <<
         | "Initial data address: " << (void*)vec.data() << std::endl;
         | int* ptr = vec.data();                std::cout << "Pointer
         | before push_back: " << (void*)ptr << std::endl;
         | std::cout << "Value via pointer before push_back: " << *ptr <<
         | std::endl;                std::cout << "\nPushing back 4...\n"
         | << std::endl;           vec.push_back(4);
         | std::cout << "New capacity: " << vec.capacity() << std::endl;
         | std::cout << "New data address: " << (void*)vec.data() <<
         | std::endl;                std::cout << "\nAttempting to access
         | data via the old pointer..." << std::endl;           std::cout
         | << "Old pointer value: " << (void*)ptr << std::endl;
         | int value = *ptr;           std::cout << "Read from dangling
         | pointer (UB): " << value << std::endl;                return 0;
         | }
         | 
         | ./a.out Initial capacity: 3 Initial data address: 0x517d2b0
         | Pointer before push_back: 0x517d2b0 Value via pointer before
         | push_back: 1
         | 
         | Pushing back 4...
         | 
         | New capacity: 6 New data address: 0x517d6e0
         | 
         | Attempting to access data via the old pointer... Old pointer
         | value: 0x517d2b0 Read from dangling pointer (UB): 20861
        
           | scoutt wrote:
           | You are not fighting the C++ compiler or showing why the C++
           | compiler might be annyoing. You are introducing a bug by
           | poorly using a library (which has nothing to do with writing
           | and compiling C++). Ergonomics I believe are fine?
           | 
           | I'm struggling hard trying to understand what or if your
           | comment has anything to do with GP's comment. Perhaps you
           | wanted to tell that the Rust compiler might have stopped you
           | from producing a buggy program, but again, it has nothing to
           | do with GP's comment.
        
             | genrilz wrote:
             | I think 0xdeafbeef is roughly recreating the first code
             | snippet from the article (which is one of the things diath
             | is complaining about) in C++ to show that the compiler
             | _should_ produce an error or else undefined behavior could
             | occur on resize.
        
           | PaulDavisThe1st wrote:
           | You opted to use features of std::vector that are documented
           | to be unsafe (notably ::data()). This is the actual C++
           | translation of the opening code in TFA:
           | #include <vector>        #include <iostream>            int
           | main() {           std::vector<int> vec = {1, 2, 3};
           | for (auto const & i : vec) {               std::cout << i <<
           | std::endl;           }       }
           | 
           | It is possible to use C++ to write unsafe code! Amazing! Some
           | people want a language where this is not possible! Great!
        
             | steveklabnik wrote:
             | > This is the actual C++ translation of the opening code in
             | TFA:
             | 
             | No, it isn't: this is iterating over references, not
             | moving. This is equivalent to                 fn main() {
             | let x = vec![1, 2];                for y in &x {
             | println!("{}", y);           }           println!("{}",
             | x.len());       }
             | 
             | in Rust. Note the &, just like in your C++.
        
               | PaulDavisThe1st wrote:
               | The purpose of the first code example in TFA:
               | 
               | > This is straightforward: we create a vector containing
               | the values [1, 2], then iterate over it and print each
               | element, and then finally print out the length of the
               | vector. This is the kind of code people write every day.
               | 
               | The C++ code I provided does essentially this (I omitted
               | printing the length, since it is so trivial), and is "the
               | kind of code people write every day".
               | 
               | The fact that Rust requires you to consider move
               | semantics for such simple code is precisely one of the
               | central points of the article.
        
               | steveklabnik wrote:
               | "C++ code that implements the problem, but in a different
               | way" is not "the actual C++ translation of the opening
               | code in TFA."
        
               | PaulDavisThe1st wrote:
               | The C++ code implements the intended goal, not the
               | problem TFA is trying to illustrate.
               | 
               | Changing between:                   for (auto i : vec)
               | 
               | and                   for (auto & i : vec)
               | 
               | has essentially no bearing on what the author is trying
               | to show. If they were focused on how move semantics are
               | always important, they would not use an integer type.
        
         | masklinn wrote:
         | > I understand why you need the into_iter bit and why the
         | borrow checker complains about it
         | 
         | The borrow checker is not really involved in the first snippet
         | (in fact the solution involves borrowing). The compiler
         | literally just prevents a UAF.
        
           | 59nadir wrote:
           | The compiler isn't protecting them from anything in this
           | particular example, we can all see this is valid code. It's
           | provably valid code, just not provable by the Rust compiler.
        
             | masklinn wrote:
             | Literally nothing in your comment is correct. In the Rust
             | snippet vector is moved into the loop, and thus freed.
             | 
             | There are situations where you hit the limits of the borrow
             | checker, plenty of them. This is not one of them. Again,
             | the borrow checker is not even involved in the original
             | snippet.
        
               | 59nadir wrote:
               | > In the Rust snippet vector is moved into the loop, and
               | thus freed.
               | 
               | Do you see any code in the snippet that requires it to
               | be? This could be a simple read-only borrow because the
               | actual logic of the program requires only that, and the
               | value could live on happily after the loop. Literally
               | nothing in this snippet requires anything else, you've
               | just sort of assumed that the way Rust does it is the
               | only thing that makes sense.
               | 
               | It's not about what Rust currently does, really, it's
               | about what it ought to do.
        
               | x-yl wrote:
               | Are you suggesting Rust should automatically insert the
               | borrow annotation because it is able to see that a borrow
               | is sufficient? That would be quite unintuitive and make
               | it ambiguous whether a for loop is borrowing or consuming
               | the iterator without reviewing the body. I'd strongly
               | argue that it should unambiguously do either one or the
               | other and not try and read the author's mind.
        
               | 59nadir wrote:
               | Yes, I'm suggesting it should do the right thing for the
               | code the loop is actually trying to execute. I personally
               | think this is exactly what Rust and its users have signed
               | up for. I might be mistaken about that, but I think it's
               | in line with the more general view that Rust is
               | attempting to be as close as it can get to a language
               | that reads like it has a garbage collector without having
               | one.
        
               | DougBTX wrote:
               | > the more general view that Rust is attempting to be as
               | close as it can get to a language that reads like it has
               | a garbage collector without having one.
               | 
               | I've used Rust a fair amount, and I've never seen that
               | expressed as a goal.
               | 
               | A couple of general principles followed by Rust are to
               | prefer explicit code over implicit conversions and to
               | support local reasoning. Those are both present here: the
               | borrow needs to be made explicitly, rather than
               | implicitly based on code later on.
        
               | ninkendo wrote:
               | It's important that a for loop takes ownership of the
               | vec, because it's the only way you can call things inside
               | the loop body that require the element itself to be
               | moved.
               | 
               | If you don't want the loop to take ownership of the vec,
               | there's literally a one character change: put a & before
               | the thing you're iterating (ie. for x in &y). That
               | borrows the vec instead of moving it.
               | 
               | You seem to want rust to decide for itself whether to
               | borrow or own the contents, but that way lies madness...
               | it will be really hard to reason about what more
               | complicated code is doing, and changes would have very
               | non-local effects on how the compiler decides to use your
               | code.
               | 
               | For me, move-semantics-by-default is _the_ key idea that
               | rust got right, and it's a very simple concept. It's not
               | intuitive, but it's the key idea behind all of rust's
               | benefits for memory management and preventing concurrency
               | bugs. "Learn one simple but non-intuitive thing and you
               | get these huge benefits" is a tradeoff I'm very much
               | willing to make, personally.
        
               | 59nadir wrote:
               | > You seem to want rust to decide for itself whether to
               | borrow or own the contents, but that way lies madness...
               | 
               | Most of what Rust does already feels like madness, like
               | the concept of implicit moves, etc., but I understand
               | your point. I don't think the reasoning really makes
               | sense in terms of actual logic, but as I wrote in another
               | comment: It's possible that I've misunderstood the sales
               | pitch of Rust trying to be GC-less GCd language.
               | 
               | > For me, move-semantics-by-default is the key idea that
               | rust got right, and it's a very simple concept. It's not
               | intuitive, but it's the key idea behind all of rust's
               | benefits for memory management and preventing concurrency
               | bugs. "Learn one simple but non-intuitive thing and you
               | get these huge benefits" is a tradeoff I'm very much
               | willing to make, personally.
               | 
               | I can respect that and seen this way (where we accept
               | that we're simply going to have unintuitive and incorrect
               | rejections of programs) it does make a lot more sense.
        
               | bombela wrote:
               | I find move by default refreshingly simple. I don't even
               | understand what is so hard to understand about move.
               | 
               | A move is a simple memcopy + the certainty that the
               | source is unreachable.
               | 
               | This is important if the memcopied object holds
               | resources.
               | 
               | Any new type is move only by default (no copy or clone).
               | This is so that you can opt in willingly.
               | 
               | Making a move type also copy is not an API/ABI breaking
               | change compared to removing copy (or clone).
               | 
               | And it's the same for clone not being the default. With
               | clone you get to run custom code for cloning instead of a
               | memcopy.
               | 
               | Remind me again how move, copy and clone works in C++ /s
        
               | SpaceNugget wrote:
               | The code says to call into_iter and consume the iteratee,
               | so rust does that. If you want a reference, use the &,
               | just like in zig/c/c++/etc. You are saying an even more
               | extreme version of "If there's a way what I wrote could
               | possibly be interpreted that could compile, it should do
               | that" ignoring the fact that there's almost assuredly
               | _many_ ways that your code can be interpreted that could
               | compile.
               | 
               | Slowing down type resolution/compilation (by making every
               | unannotated loop a generic T|&T) and adding more syntax
               | (since rust would need new annotations to explicitly
               | specify borrow/take in for loops), in order to save a
               | single character that matches the behavior of most other
               | related languages and is perfectly clearly explained by
               | the compiler, is maybe a bad move. Considering compile
               | time and complicated syntax are two of the biggest things
               | people who actually write rust complain about.
        
         | throwawaymaths wrote:
         | i think really the problem is not the compiler so much but that
         | you need to be thinking about what `for` desugars to.
        
           | dbdr wrote:
           | The desugaring is an explanation of what's happening under
           | the hood, but you don't _need_ to think about that. Your
           | mental model can simply be:                   for _ in x  //
           | Consumes (moves) x and iterates over the values in x
           | for _ in &x // Borrows x and iterates over references to the
           | values in x
        
             | throwawaymaths wrote:
             | depends on what `into_iter` does in your type impl
             | 
             | ive always felt that there is something a bit strange about
             | a keyword reaching into the stdlib (rust is not the only
             | language that does this), especially if it's only for
             | sugaring.
        
       | rollcat wrote:
       | I'm starting to think Zig's strategy to memory management is
       | generally friendlier to a developer. If a function needs to
       | allocate memory, it must take an allocator as a parameter. If it
       | needs scratch space to perform computation, it can use that
       | allocator to create an arena for itself, then free it up before
       | it returns (defer). If it returns a pointer, the caller should
       | assume the object was allocated using that allocator, and becomes
       | the owner. It may still be unclear what happens to a pointer if
       | it's passed as a parameter to another function, but I'd normally
       | consider that a borrow.
       | 
       | It's a lot of assumptions, and if you trip, Rust will yell at you
       | much more often than Zig; and it will likely be right to do so.
       | But in all seriousness, I'm tired of the yelling, and find Zig
       | much more pleasant.
        
       | gitroom wrote:
       | been banging my head against this same stuff trying to learn rust
       | - honestly memory rules make me miss how easy c feels sometimes,
       | but i'm sticking with it cuz i want fewer bugs
        
         | pjc50 wrote:
         | All of the same rules exist in C, they're just tracked manually
         | inside the programmer's head and you don't find out about
         | mistakes until much later.
        
       | dmitrygr wrote:
       | > So why does this result in a move and why does replacing x with
       | &x fix it
       | 
       | Precisely the sort of question I do not want to waste time on.
        
       ___________________________________________________________________
       (page generated 2025-05-06 23:02 UTC)