Skip to content

Add [_]::shift_{left,right} #717

@scottmcm

Description

@scottmcm

Proposal

Problem statement

There's currently no efficient and safe way to remove from a slice and insert a replacement value at a different location, in general. It's some easy unsafe code, but it's be nice to offer it in a more optimized version than rotate+replace (since rotate, while linear, is more expensive than `memmove).

Motivating examples or use cases

Inspired by the conversation in #t-libs > `remove_insert() for `Vec`, or more generally for `&mut [T]` @ 💬.

They specifically wanted to optimize remove+insert on a Vec-like type, to avoid two large memmoves when the positions are close by. But this can be done on slice instead, making it more generally applicable as a building block plus simplifing the interface from what was originally discussed there.

Solution sketch

impl<T> [T] {
    pub fn shift_left<const N: usize>(&mut self, inserted: [T; N]) -> [T; N];
    pub fn shift_right<const N: usize>(&mut self, inserted: [T; N]) -> [T; N];
}

Example:

let mut a = [1, 2, 3, 4, 5];
assert_eq!(a.shift_left([7, 8]), [1, 2]);
assert_eq!(a, [3, 4, 5, 7, 8]);

let mut a = [1, 2, 3, 4, 5];
assert_eq!(a.shift_right([7, 8]), [4, 5]);
assert_eq!(a, [7, 8, 1, 2, 3]);

This would be

  • trivially a nop for N == 0, just like << 0 and >> 0.
  • panic for N > self.len().
  • same as self.as_mut_array().replace(inserted) for N == self.len().
  • handles the scalar case just fine via .shift_left([3]) without adding overhead.
  • always implemented with
    • ptr::read the returned values
    • ptr::copy (overlapping!) the middle N places
    • ptr::write the inserted values
    • none of which can panic, so there's no droppage concerns.
  • named to match the existing rotate_left+rotate_right, as the same intuitions apply

Alternatives

  • Only support a single value
    • but that makes it less general as a building block, and supporting arrays doesn't make it any less efficient
  • Allow passing in the insert/remove positions
    • but that makes the interface more complex (two usizes mean potential confusion over which is which), isn't necessary (since one can always sub-slice the slice), and would add cost in the implementation (which would need to compare the positions and branch to decide what to do)
  • Do nothing
    • certainly always possible, since others can write this themselves, but I do think a "safe memmove" is a valuable offering for core

Links and related work

rust-embedded/heapless#635

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions