Skip to content

ACP: Add extend_front, extend_from_within, extend_from_within_front, prepend and splice to VecDeque #658

@antonilol

Description

@antonilol

Proposal

Problem statement

Switching from Vec to VecDeque because efficient push and pop is needed at both ends can cost performance when already using methods like Vec::extend_from_within and Vec::splice. Such methods are not available on VecDeque.

Motivating examples or use cases

This came up in a self growing stack data structure where elements can be pushed and popped (from the back of the stack). In case there are no more items new items are created just in time. One can also get the last n elements (this calls self.grow_to(n), see below) as a slice. This currently uses Vec but I want to use VecDeque here so grow_to does not have to shift all elements. While I use splice here, this is actually a special case where extend_front would be the best. extend_from_within is also used in here, which currently does not exist on VecDeque.

pub(crate) struct Stack {
    elements: Vec<Expr>,
    next_element_id: u16,
}

impl Stack {
    fn grow_to(&mut self, min_len: usize) {
        if self.elements.len() >= min_len {
            return;
        }

        let to_insert = min_len - self.elements.len();
        self.next_element_id += to_insert as u16;
        self.elements.splice(
            0..0,
            (0..to_insert)
                .map(|i| Expr::Stack(StackExpr::new(self.next_element_id - i as u16 - 1))),
        );
    }

    pub fn extend_from_within_back(&mut self, amount: usize, offset: usize) {
        self.grow_to(amount + offset);

        let to = self.len() - offset;
        let from = to - amount;
        self.elements.extend_from_within(from..to);
    }

    // ...
}

Solution sketch

pub struct Splice<...> { ... } // like vec::Splice but for VecDeque

impl<T> VecDeque<T> {
    pub fn prepend(&mut self, other: &mut Self);

    pub fn splice<R, I>(&mut self, range: R, replace_with: I) -> Splice<'_, I::IntoIter, A>
    where
        R: RangeBounds<usize>,
        I: IntoIterator<Item = T>;
}

impl<T: Clone> VecDeque<T> {
    pub fn extend_from_within<R>(&mut self, src: R)
    where
        R: RangeBounds<usize>;

    pub fn extend_from_within_front<R>(&mut self, src: R)
    where
        R: RangeBounds<usize>;
}

Not sure about extend_front yet (asked these questions in rust-lang/rust#146861 before, but they should really be in this ACP):

  • extend allows iterators that yield &T where T is Clone, should extend_front do too?
  • Does this need a whole new trait like Extend or only a method on VecDeque?
  • Should extend_front act like repeated pushes to the front of the queue? This reverses the order of the elements. Doing it different might incur an extra move if the iterator length is not known up front (where do you start placing elements in the buffer?).

Alternatives

All these methods can be written in terms of existing APIs, but may not perform optimally. As an example, extend_from_within can be emulated by cloning the needed elements to a new collection and then using VecDeque::extend. This needs an additional copy and collection. splice also looks costly to emulate, as far as I can tell.

prepend could be written in a separate crate, but that crate would need to reimplement private VecDeque helpers like to_physical_idx. The rest relies on specialization, so would need explicit methods like VecDequeExt::extend_from_within_copy to match the performance.

Links and related work

Feature request: rust-lang/rust#69939
First try at implementing extend_front: rust-lang/rust#146861

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

    ACP-acceptedAPI Change Proposal is accepted (seconded with no objections)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