[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)