Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iterate slice by reference (not by value) #14800

Open
ysbaddaden opened this issue Jul 8, 2024 · 1 comment
Open

Iterate slice by reference (not by value) #14800

ysbaddaden opened this issue Jul 8, 2024 · 1 comment

Comments

@ysbaddaden
Copy link
Contributor

Iterating a Slice(T) with #each leads to iterating the slice by value. This is usually fine when T is a primitive (e.g. UInt8) or a Reference to a class instance, but when T is a struct it leads to getting a copy. Sometimes this is what we want, but at other times we'd like to avoid the copy and/or need to mutate the struct in-place.

This stems from the nature of Ruby where everything's supposed to be a Reference or a primitive, not a value that is passed by copy (except for primitives).

To solve this, we currently need to get the raw pointer and loop/increment it manually, but the intent ain't immediately explicit, in addition to be ugly Crystal:

slice.size.times do |i|
  ptr = slice.to_unsafe + i
  # ...
end

We could at least abstract a method, for example #each_ref(&) or #each_reference(&). We'd still have to deal with a Pointer(T), but at least the iteration part would be much more readable:

struct Slice(T)
  def each_ref(&)
    size.times { yield to_unsafe + i }
  end
end

slice.each_ref do |ptr|
  # ...
end
@straight-shoota
Copy link
Member

straight-shoota commented Aug 9, 2024

I found this post talking about a similar problem with Go: https://jvns.ca/blog/2024/08/06/go-structs-copied-on-assignment/. The story of that blog post is that the author previously hadn't even noticed that structs are copied on assignment and it came up in an iteration situation.

In Go you can explicitly pass structs by reference with taking a pointer (&thing, return type *Thing). Apparently dereferencing happens implicitly, so this is quite transparent for the receiver.

Anyway, the loop in the following function still caused an implicit copy, so the returned value is not a reference to the array item but a copy from the loop assignment (note: because the function returns a reference to this copy, it cannot be placed on the stack).

func findThing(things []Thing, name string) *Thing {
  for _, thing := range things {
    if thing.Name == name {
      return &thing
    }
  }
  return nil
}

This is simple to resolve by changing the iteration style. Apparently taking the pointer directly from an array index accessor doesn't involve a copy and it directly returns a reference to the array item:

func findThing(things []Thing, name string) *Thing {
  for i := range things {
    if things[i].Name == name {
      return &things[i]
    }
  }
  return nil
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants