Hacker News new | ask | show | jobs
by wtetzner 2689 days ago
Just out of curiosity, do you have an example where Rust got in your way when trying to use an array slice?
1 comments

Approximately, here is one example. I wanted to use something like a Vec<&[u8]> as a means of passing a set of buffers to a function. Likewise it made sense to return such a value from another function, g. The way g worked was to read or hold a large buffer into memory, e.g. 1 megabyte in size, and then parse out the slices from it. So maybe you'd want to return something that looks like the C++ type

    struct Foo {
    private:
        vector<uint8_t> buf;
    public:
        // points into buf
        vector<pair<const uint8_t *, size_t>> slices;
    };
Well you can't do that. Obviously there are workarounds, like to return an object of type Foo holding the buf, with an api like impl Foo { fn getSlices(&self) -> Vec<&[u8]> }. Internally the object holds a Vec<(usize, usize)> or something like that, and you have a bunch of translation logic around your API's. And extra work to allocate the return value. So that's one example of Rust getting in your way.

Probably some others would be uses of Interval<Buf>, and some cases where functions return Buf in https://github.com/srh/nihdb . I wanted to use &[u8] to represent the bounds of intervals, and IIRC that generally involved passing intervals upwards into functions, but for some reason I can't remember it got annoying and I couldn't be bothered to do it.

This is a pain point, but it’s not due to slices; it’s the “self-referential struct problem.” There are various solutions, as you note, but it can be annoying, it’s true. It’s a tough one; in the general case, it’s saving you from problems, but when you know you’re not going to hit those edge cases, it’s less than ideal.
Is there a theoretical reason for the "self-referential struct problem", or is it just an artifact of the current borrow-checker implementation?

It doesn't seem unsafe to have a struct field refer to another member of that struct, but maybe I'm missing something.

It's not theoretical, it's practical. Here's some code with a self-referential struct:

https://play.rust-lang.org/?version=stable&mode=debug&editio...

Here, we have a self-referential struct. If you run this, you may get different numbers than me, but

  [src/main.rs:17] &f = Foo {
      x: 5,
      p: 0x00007ffcbbba41c0
  }
Here, p points to x. It's all good. The address of f is

  [src/main.rs:19] &f as *const Foo = 0x00007ffcbbba41b8
We move f into oh_no. Its address changes:

  [src/main.rs:25] &f as *const Foo = 0x00007ffcbbba3f28
... but p does not:

  [src/main.rs:26] &f = Foo {
      x: 5,
      p: 0x00007ffcbbba41c0
  }
Any access of p is now a use-after-free.

Does that make sense?

OK, so the issue is that references aren't updated when a move occurs. That does make sense.

So to make this work, references would need to be re-written when a move/copy occurs.

I can still see having an easy way to construct self-referential structs being a useful thing, even if the compiler prevents you from moving them. Maybe with a smart clone() method that can update references correctly.

However, I am a little confused about this specific example. I don't understand why oh_no() taking ownership causes f to be copied to a new location. Shouldn't it remain in the same place on the stack? I feel like I'm missing something.

> So to make this work, references would need to be re-written when a move/copy occurs.

Yes! C++ has a concept called "move constructors" that allows for this (this would be that "smart clone" you talk about later in the comment), but we made a decision to not include it. This introduces some nice properties, at the cost of disallowing self-referencing structs.

> I can still see having an easy way to construct self-referential structs being a useful thing, even if the compiler prevents you from moving them.

So, in some sense, this is what the new Pin stuff is about. It lets you say "from this point on, this thing isn't going to move again" and therefore be self-referential.

> I don't understand why oh_no() taking ownership causes f to be copied to a new location.

"Taking owernship" means "move". "move" means "a memcpy from the old to the new location." Rust isn't really special from any other sort of language with value semantics here, other than disallowing you to use the old value, since it was moved out from. Does that make sense?

I didn't say it was specific to slices. The inconvenience around array slices is because they have a pointer. The non-pointery aspects of Rust slices are an advantage over C++. (E.g. not having pointer arithmetic.)
The thread started out by saying slicing, specifically.

Regardless, no worries, I was just trying to add clarity around details, just in case.