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

RFC: Adding ArrayVec to core #3316

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft

Conversation

tgross35
Copy link
Contributor

@tgross35 tgross35 commented Sep 15, 2022

Rendered

This RFC seeks to add a representation for variable-length data within a fixed-size array, with an interface similar to Vec. Goals are to unite and clarify existing concepts in core, provide language users with a representation for a common data structure that is not heap-depdent, and ease some FFI relationships.

Current use (subject to change) looks like:

use core::collections::ArrayVec;

const BUF_LEN: usize = 16;

let mut v: ArrayVec<i32, BUF_LEN> = ArrayVec::new();
v.push(1).unwrap();
v.push(2).unwrap();

Biggest issues to address

  • 1. Location: core::collections (new), core::array (existing), and core::ext (nex) have all been offered as suggestions
  • 2. to panic or not to panic simple operations like push() that will never (reasonably) fail for Vec are extremely failable for ArrayVec. I elected to make all potentiall failable methods return a Result or similar, based on the belief that core should attempt to never panic (in no_std environments, panicking is generally UB) Any API should mirror Vec's existing API as much as possible, with possible additions for checked operations
  • 3. Generics interface: @thomcc brought up the possibility of using ArrayVec<[T; N]> with deref traits to allow for some code reuse, see the comment here. I personally think that the interface has some more potential for confusion than ArrayVec<T, N>, but the benefits are notable (I don't know enough to know if the code duplication comes from a compiler limitation or a language limitation).
  • 4. Slice backing: I see notable benefits to allowing an ArrayVec (or perhaps more accurately in this case, BufVec) being able to be backed by any slice, rather than just strictly arrays. I have yet to come up with a clean possible implementation for this, but think it is worth doing if it can be done without overhead or added confusion

Links

@thomcc
Copy link
Member

thomcc commented Sep 15, 2022

I don't know enough to know if the code duplication comes from a compiler limitation or a language limitation

The code duplication is unlikely to be resolvable via either language or compiler improvements. It's somewhat fundamental, as the code won't be 100% identical (it's similar to taking &[T; N] vs &[T]).

I have yet to come up with a clean possible implementation for this, but think it is worth doing if it can be done without overhead or added confusion

That is similar to what I proposed, although perhaps I'm misunderstanding.

@Lokathor
Copy link
Contributor

The tinyvec crate has a SliceVec type, if that's any help or inspiration.

@tgross35
Copy link
Contributor Author

tgross35 commented Sep 15, 2022

The code duplication is unlikely to be resolvable via either language or compiler improvements. It's somewhat fundamental, as the code won't be 100% identical (it's similar to taking &[T; N] vs &[T]).

Thank you for the clarification, that nuance is out of my league so I leave it to you & the other maintainers to decide

That is similar to what I proposed, although perhaps I'm misunderstanding.

I think it is possible with your implementation (which I mentioned in the doc) but just isn't in your linked example since there's no constructor for ArrVec<[T]>

The tinyvec crate has a SliceVec type, if that's any help or inspiration.

Yes that was exactly my thought! It looks like TinyVec uses Thom's implementation, so maybe that's a good place to start and just add the unsafe optimizations

@programmerjake
Copy link
Member

programmerjake commented Sep 15, 2022

an alternative is the storages proposal (wg-allocators issue) that allows an ArrayVec to be written Vec<T, [MaybeUninit<T>; N]> where an array is used instead of an allocator, the array is stored inline in the Vec rather than being allocated on the heap. you can also do a whole lot more with storages, e.g. implement a storage that stores a few elements inline and if there's too many stores them on the heap instead, you can also use a &mut [MaybeUninit<T>] as a storage, allowing reusing an existing buffer.

also, I think ArrayVec is a better name than StackVec since StackVec is imho a misleading name, it can easily be stored on the heap: Box<StackVec<[T; N]>> or Arc<MyStructContainingAStackVec>

also there's two Rendered links in the top comment, the broken one at the bottom should be removed.

@tgross35 tgross35 changed the title RFC: Adding StackVec to core RFC: Adding ArrayVec to core Sep 15, 2022
@tgross35
Copy link
Contributor Author

an alternative is the storages proposal that allows an ArrayVec to be written Vec<T, [MaybeUninit<T>; N]> where an array is used instead of an allocator, the array is stored inline in the Vec rather than being allocated on the heap. you can also do a whole lot more with storages, e.g. implement a storage that stores a few elements inline and if there's too many stores them on the heap instead, you can also use a &mut [MaybeUninit<T>] as a storage, allowing reusing an existing buffer.

That is extremely clean, great suggestion. core itself and anything no_std don't stand to benefit without some moving around, correct? And, is it possible to use a slice instead of an array as the allocator here? (those answers may be in the thread, I am still reading through)

also, I think ArrayVec is a better name than StackVec since StackVec is imho a misleading name, it can easily be stored on the heap: Box<StackVec<[T; N]>> or Arc<MyStructContainingAStackVec>

You are correct, ArrayVec is what is in the document. I just named my branch off the original proposal and named the PR wrong after it. Corrected the title

also there's two Rendered links in the top comment, the broken one at the bottom should be removed.

Thanks for the catch, fixed this

@programmerjake
Copy link
Member

an alternative is the storages proposal that allows an ArrayVec to be written Vec<T, [MaybeUninit<T>; N]> where an array is used instead of an allocator, the array is stored inline in the Vec rather than being allocated on the heap. you can also do a whole lot more with storages, e.g. implement a storage that stores a few elements inline and if there's too many stores them on the heap instead, you can also use a &mut [MaybeUninit<T>] as a storage, allowing reusing an existing buffer.

That is extremely clean, great suggestion. core itself and anything no_std don't stand to benefit without some moving around, correct?

yes. i'd expect Vec and String (and maybe more) to move to core if storages are implemented. the only difference in alloc and std is that the type aliases for Vec and String would have the storage type argument default to the global allocator.

And, is it possible to use a slice instead of an array as the allocator here? (those answers may be in the thread, I am still reading through)

yes, that's what the &mut [MaybeUninit<T>] storage is.

@tgross35
Copy link
Contributor Author

Wow, that concept really blows this right out of the water and I wish I had seen it earlier. It seems a bit like discussion has stalled unfortunately, the linked thread ended over a year ago and this one hasn't seen too much activity

I will do some poking on it tomorrow

assert_eq!(v[0], 7);

// Many higher-order concepts from `Vec` work as well
v.extend([1, 2, 3].iter().copied());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting one, since it has the same "panic vs truncate vs …" question that collecting has.

It reminds me of the discussions in #3271 (comment) -- maybe there could be a similar API for "extend or push or … within existing capacity" between normal Vec and ArrayVec.

@scottmcm
Copy link
Member

based on the belief that core should attempt to never panic

This is an overstatement. There's even panics in built-ins, like [1][1].

That doesn't mean these APIs should panic, but I don't think it can be immediately discarded.

@SkiFire13
Copy link

Generics interface: @thomcc brought up the possibility of using ArrayVec<[T; N]> with deref traits to allow for some code reuse, see #2990 (comment). I personally think that the interface has some more potential for confusion than ArrayVec<T, N>, but the benefits are notable (I don't know enough to know if the code duplication comes from a compiler limitation or a language limitation).

Couldn't the same be done with ArrayVec<T, N> and a separate type that it derefs to?

@BurntSushi
Copy link
Member

RE panics: I'd probably prefer that we have both push and try_push, for example. It mirrors Vec and I think that's worth something.

RE storage: It would be quite amazing to have Vec<T, [MaybeUninit<T>; N]> be a thing, and it does kind of sound like that would completely supplant any hypothetical ArrayVec. So maybe we need to make progress on the storages proposal first to see whether that's something that can move forward.

I am overall 👍 at least on the idea of having an abstraction like this in core though.

@CAD97
Copy link

CAD97 commented Sep 15, 2022

It seems a bit like [Storage] discussion has stalled unfortunately

I'm working on it, and will be bringing forward a project proposal early October. https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/Recruiting.3A.20Storage.20API.20Project.20Group

@SimonSapin
Copy link
Contributor

I think the generic storage idea is neat and might have some use cases, but it’s well into diminishing-returns territory IMO in terms of how complex it is compared to what it brings. It could live in a crate for the (comparatively) few cases where it’s useful, while more specific Vec and ArrayVec would be in the standard library.

@programmerjake
Copy link
Member

I think the generic storage idea is neat and might have some use cases, but it’s well into diminishing-returns territory IMO in terms of how complex it is compared to what it brings. It could live in a crate for the (comparatively) few cases where it’s useful, while more specific Vec and ArrayVec would be in the standard library.

imho storages would be far more useful specifically because it would fit into the standard library's types in the way allocators currently do in nightly, that wouldn't really be possible just using an external crate because you couldn't use it with the standard types then, you'd have to specifically opt into using those storage-enabled types which is much less likely to happen throughout the crate ecosystem.

with storages in the standard library 3rd-party crates could easily use them while still working with the standard types:

fn third_party_code<S: Storage>(v: &mut Vec<i32, S>) { ... }

// works great:
let mut my_vec = vec![1, 2, 3];
third_party_code(&mut my_vec);

that would not work with storages being a 3rd-party crate because the Vec and other types they would have to use won't be the standard Vec types.
the below code can't work, so third party crates won't try to be generic over storages:

fn third_party_code<S: storages::Storage>(v: &mut storages::Vec<i32, S>) { ... }

// can't work:
let mut my_vec = vec![1, 2, 3];
third_party_code(&mut my_vec); // type mismatch Vec != storages::Vec

@thomcc
Copy link
Member

thomcc commented Sep 15, 2022

Couldn't the same be done with ArrayVec<T, N> and a separate type that it derefs to?

Not really, the cleverness there is actually not the deref but the use of unsizing. Actually, I'm not sure why the deref is needed -- it's surprising that deref is needed at all there. It shouldn't be, but it seems to help method resolution perform the unsizing coersion.

@ehuss ehuss added the T-libs-api Relevant to the library API team, which will review and decide on the RFC. label Sep 15, 2022
@tgross35
Copy link
Contributor Author

tgross35 commented Sep 15, 2022

I love the storages idea a lot, especially because I felt greedy even mentioning the possibility of ArrayString in this RFC - with storages, you get that for free. And any other collections, I'm actually really curious what the future of core and std will look like here since you could move a lot to core with this change.

@CAD97 is there anything publicly available to help shape the storages design, or are you sort of attempting to get something working quietly before discussion adds noise? (completely understandable if so).

Re: push -> Result or push -> maybe panic, I will adjust the RFC for the changes proposed. Perhaps my belief that core should never panic (comes from Linus's comment on allocating, and from my own work on embedded) would be better redirected into "there is advantage to presenting a non-panicking API for a lot of things". Curious how it will propegate to storages where your allocator may very llikely run out of space, and a lot of Vec usage don't really expect that.

I will keep this RFC around as a draft while we see storages develop, because having a "desirable" API for this sort of construct will probably help shape the goals there, things like methods to construct the object on an existing memory buffer (and, I suppose, if storages wind up not making it - which I hope isn't the case). And it probably wouldn't be totally unreasonable to provide something like type ArrayVec<T, N> = Vec<T, [MaybeUninit<T>; N]>; to help guide users. (also makes me wonder where Thom's comments about asm duplication fall into storages).

@dlight
Copy link

dlight commented Sep 16, 2022

an alternative is the storages proposal (wg-allocators issue) that allows an ArrayVec to be written Vec<T, [MaybeUninit<T>; N]> where an array is used instead of an allocator, the array is stored inline in the Vec rather than being allocated on the heap.

What if ArrayVec is introduced now as a new type, and if the storages proposal is accepted it is changed into a type alias?

@CraftSpider
Copy link

If ArrayVec is stable, that can't be done in a backwards-compatible way most likely. If any downstream has impl Foo for ArrayVec and impl Foo for Vec, they may conflict, particularly if they're generic over the storage on Vec already. I think there may be other cases, but that's the first that comes to mind.

@Xiretza
Copy link

Xiretza commented Sep 16, 2022

What if ArrayVec is introduced now as a new type, and if the storages proposal is accepted it is changed into a type alias?

See four comments above: #3316 (comment)

@tarcieri
Copy link

tarcieri commented Sep 16, 2022

@CraftSpider

As @CAD97 mentioned above, since the second generic parameter of Vec<T, A> which specifies the Allocator (as well as the Allocator trait itself) are unstable (gated behind the allocator_api feature), it's possible to stabilize ArrayVec in such a way it can be "upgraded" to a type alias later when that generic parameter of Vec is stabilized.

Unless that parameter is stable, it's not possible to write a conflicting impl on stable Rust, since any impl you can write today on stable Rust uses the default A = Global.

@programmerjake
Copy link
Member

it should be possible to upgrade Vec<T, A> where A: Allocator and a separate ArrayVec<T, N> to Vec<T, S> where S: Storage and type ArrayVec<T, const N: usize> = Vec<T, [MaybeUninit<T>; N]> if there's the impl:

impl<T, const N: usize> !Allocator for [T; N] {}

@CAD97
Copy link

CAD97 commented Sep 16, 2022

(whether Allocator: Storage is undecided; I personally lean towards having a wrapper AllocStorage<A>. additionally, it's almost certainly going to be InlineStorage rather than directly an array, because Storage is (now) an untyped interface to memory. i'd also prefer InlineStorage<SIZE, ALIGN> (or better, a const Layout) to InlineStorage<[T; N]> but unfortunately it's not possible to #[repr(align(CONST))] yet.)

(but yes; theoretically, if there is a blanket implementation from Allocator to Storage, it would be possible to stabilize being allocator generic and then later generalize to storage generic. however, it would likely still require using a newly stabilized ArrayStorage type rather than just raw arrays for array storage, as std doesn't bound the generic on the struct definition (only on the impls) so it's perfectly allowed to name (and implement traits on) e.g. Vec<T, ()> despite () not implementing Allocator.)

@programmerjake
Copy link
Member

however, it would likely still require using a newly stabilized ArrayStorage type rather than just raw arrays for array storage, as std doesn't bound the generic on the struct definition (only on the impls) so it's perfectly allowed to name (and implement traits on) e.g. Vec<T, ()> despite () not implementing Allocator.)

that seems to error out for me:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=09313310ddda8252d11bce5bdf7e853c

can you make an example that doesn't error?

@burdges
Copy link

burdges commented Sep 17, 2022

There are several ecosystem crates doing this because they all provide slightly different functionality, although perhaps their functionality merges somewhat with min_const_generics. Imho the prior art section should map out the differences in more detail, and give some idea why core should be making a particular choice.

As @SimonSapin indicates above, I doubt Vec<T, [MaybeUninit<T>; N]> ever makes much sense for several reasons:

  • RawVec spends two extra usize while ArrayVec avoids wasting that memory. It's perhaps okay doing this if you only need an ephemeral tool for constructing [T; N] but becomes unacceptable if using ArrayVec inside other types.
  • Vec::into_box must correctly convert these two usize into the metadata required by Box somehow, or more likely A: <[MaybeUninit<T>; N] as Allicator> adds this metadata separately, even further increasing the overhead of Vec<T, [MaybeUninit<T>; N]>.
  • ArrayVec::into_inner must strip this Allicator metadata, two usize before, and one len: usize after the RawVec.alloc: A, but ideally all this should wind up without copies in practice. ArrayVec itself does not necessarily do this correctly either, but it's simpler smaller overhead looks easier for optimizing code gen to handle correctly.

Around these concerns, one likely needs reference analogs of ArrayVec::into_inner for usage inside other types. I do not know if one of the ecosystem crates handles this better somehow.

We do need to understand the design space if this is going into core. We do not necessarily need a core::ArrayVec that does everything though. In fact, one often wants to stabilizable essential functionality, like ArrayVec::into_inner(self) -> [T; N], along with opinionated unstable extensions, but avoid conflicts between other conflicting extensions.

@burdges
Copy link

burdges commented Sep 17, 2022

Around representations.. heapless::Vec looks exactly like arrayvec::ArrayVec, and staticvec::StaticVec is similar except for using a MaybeUninit<[T; N]> like the RFC proposes. It appears const generics really did homogenize these creates somewhat.

There exist some reasons for using a [T; N] type parameter instead of a separate const CAP: usize sometimes, but maybe none apply here. If T is a ZST then [T; N] is a ZST too, which is desirable, not sure which crates handle ZSTs correctly.

It appears tinyvec::Arrayvec and smallvec::SmallVec differs by using an Array trait instead of const generics. It's clear smallvec needs an Array trait for its heap overflow feature, about which we do not care. Avoiding unsafe like tinyvec is also irrelevant to us.

@SimonSapin
Copy link
Contributor

SmallVec could and should switch to const generics instead of an Array trait, it just hasn’t yet. You can think of it as this (but with some layout optimizations):

enum SmallVec<T, const N: usize> {
    Inline(ArrayVec<T, N>),
    Heap(Vec<T>),
}

@contactomorph
Copy link

Am I the only one who thinks that ArrayVec is a terrible name? To me it obviously suggests a two-dimensional collection: a vector of arrays. If I had to describe the proposed collection I would go for something like PartiallyFilledArray. Arguably this one is too long but at least it is descriptive. If this RFCs is accepted I really hope far more bikeshedding will be done about the name.

@burdges
Copy link

burdges commented Sep 20, 2022

Any thoughts on [MaybeUninit<T>; N] vs MaybeUninit<[T; N]>?

As I noted above, we should add fn ref_inner(&self) -> Option<&[T; N]> and fn mut_inner(&mut self) -> Option<&mut [T; N]> as into_inner analogs, assuming folks like those names.

We maybe need a post-const-generics summary of the real differences between the ecosystem crates (or their ideal targets)?

Is there some need for a Vec backed by a &mut [MaybeUninit<T>]? If so, I guess it'd differ from these, and be some new ecosystem crate.

@jdahlstrom
Copy link

Am I the only one who thinks that ArrayVec is a terrible name? To me it obviously suggests a two-dimensional collection: a vector of arrays.

"FooBar" meaning "a Bar backed by Foo" has precedents in std: VecDeque (as well as BTreeMap &co.)

@kpreid
Copy link
Contributor

kpreid commented Sep 22, 2022

The RFC proposes

    pub unsafe fn from_raw_parts_mut<'a, T, const N: usize>(
        data: *mut [T; N],
        len: usize,
    ) -> &'a mut Self<T, N>

but this function is not implementable because the len can't at this point be put in memory next to the data, where it needs to be for an &mut ArrayVec.

A different type (or generic storage that can be a pointer) would be needed to implement this specific use case.

@SimonSapin
Copy link
Contributor

Agreed. from_raw_parts and into_raw_parts on Vec exist to transfer ownership of an heap allocation in and out of Vec, but there is no heap allocation in ArrayVec.

ArrayVec could potentially be constructed from an array by value instead of behind a pointer: fn from_raw_parts(data: [MaybeUninit<T>; N], len: usize) -> Self but having to build the array first and then move it doesn’t seem desirable.

Instead, I think that having these Vec methods on ArrayVec could fulfill the role of low-level unsafe access:

And if accessing the (entire) storage as a fixed-sized array is useful, maybe also:

  • pub fn storage_mut(&mut self) -> &mut [MaybeUninit<T>; N]

I’m not sure what the RFC means by “to easily map to C's void somefunc(int buf[BUF_SIZE], int* len)”. Is somefunc meant to write to buf and len?

@programmerjake
Copy link
Member

I’m not sure what the RFC means by “to easily map to C's void somefunc(int buf[BUF_SIZE], int* len)”. Is somefunc meant to write to buf and len?

That looks like it would be trivially solved by storages by using
Vec<c_int, &mut [MaybeUninit<c_int>; BUF_SIZE]>. ArrayVec doesn't have a way to do that since it contains its backing array, rather than a reference to an array.

@tgross35
Copy link
Contributor Author

"FooBar" meaning "a Bar backed by Foo" has precedents in std: VecDeque (as well as BTreeMap &co.)

In this case, would VecArray be better? (Probably don't need to address this anytime soon)

Is there some need for a Vec backed by a &mut [MaybeUninit]? If so, I guess it'd differ from these, and be some new ecosystem crate.

The more I think about it, the more I think the answer is yes. Which means this RFC needs some big-ish tweaks. Reasoning:

  • Some design patterns are completely reliant on using an existing buffer (e.g., replies to the quotes below)
  • Electing to do it now is easy, doing it later may not be possible
  • Thom's design uses this concept, allows leveraging of sizing

but this function is not implementable because the len can't at this point be put in memory next to the data, where it needs to be for an &mut ArrayVec.

A different type (or generic storage that can be a pointer) would be needed to implement this specific use case.

ArrayVec could potentially be constructed from an array by value instead of behind a pointer: fn from_raw_parts(data: [MaybeUninit; N], len: usize) -> Self but having to build the array first and then move it doesn’t seem desirable.

Ah, good catch. Some sort of slice or reference-backed option would be needed to solve this, so I will be updating the RFC.

My thoughts on construction aren't currently clearly expressed super well, but basically I am hoping to be able to construct an ArrayVec on any of these locations for the most usefulness:

  • On a new array (the default for new())
  • On an existing array (so, this would be reliant on having a & implementation which could be a slice)
  • At an arbitrary memory buffer identified by a pointer and a const length (this would just be a FFI helper method to perform the above without needing to cast)
  • On an existing slice (Reliant on an ArrayVec<[T]>)
  • At an arbitrary memory buffer identified by a pointer and runtime length (so a slice, just a helper to construct the above)

I’m not sure what the RFC means by “to easily map to C's void somefunc(int buf[BUF_SIZE], int* len)”. Is somefunc meant to write to buf and len?

That's correct, this is the pattern used when the callee is expected to manipulate items within buf, possibly adding or removing values. If the callee is only intended to fill the buffer (i.e., the buffer is known to be empty), returning the length instead is more common (int fill_an_array(datatype buf[BUF_SIZE])). These patterns just tend to be a lot more common in C than in C++ or Rust because of the lack of a vector equivalent.

Even the simple string.h functions like strcat and strcpy kind of use this pattern, but they just use the position of \0 instead of returning or modifying a length.

@SimonSapin
Copy link
Contributor

That's correct, this is the pattern used when the callee is expected to manipulate items within buf, possibly adding or removing values. If the callee is only intended to fill the buffer (i.e., the buffer is known to be empty), returning the length instead is more common (int fill_an_array(datatype buf[BUF_SIZE])). These patterns just tend to be a lot more common in C than in C++ or Rust because of the lack of a vector equivalent.

Right, that’s what I thought. So what’s needed to support these use cases is not constructing a brand new ArrayVec from an arbitrary existing pointer (is ownership of the values transferred or should they be cloned? is the pointer a heap allocation that the constructor should deallocate?), but rather supporting sequences of operations like:

  1. creating a new ArrayVec somewhere: new
  2. getting a pointer to its data: spare_capacity_mut, split_at_spare_mut, storage_mut
  3. unsafe writes (or reads) through that pointer, possibly via an FFI call
  4. setting the new length: set_len

@tgross35
Copy link
Contributor Author

  1. creating a new ArrayVec somewhere: new
  2. getting a pointer to its data: spare_capacity_mut, split_at_spare_mut, storage_mut
  3. unsafe writes (or reads) through that pointer, possibly via an FFI call
  4. setting the new length: set_len

Thanks for the clarification, this all handles the "Rust is caller" situation (with as_ptr too) but I did not document that well. Handling the "Rust is callee" situation would be possible via from_raw_parts if there is a slice-backed option

@SimonSapin
Copy link
Contributor

If Rust is called with this kind of C-like API, ArrayVec (especially one owned by the callee) doesn’t feel appropriate to me. The semantics of the call seem closer to passing &mut [MaybeUninit<T>] so I’d go with creating that with std::slice::from_raw_parts_mut(ptr.cast(), len) and calling MaybeUninit::write like slice[index].write(value)

@burdges
Copy link

burdges commented Sep 26, 2022

It's unclear if the Vec variants backed by an owned [MaybeUninit<T>; N] and a &mut [MaybeUninit<T>] should really be the same type, but..

Someone could build a VecSlice crate backed by BorrowMut<[MaybeUninit<T>]> to see how the design suffers.

pub struct VecSlice<T, B: ?Sized+BorrowMut<[MaybeUninit<T>]>> {
    // NOTE order is important for optimizations. the `len` first layout lets the compiler optimize
    // `new` to: reserve stack space and zero only the first word. 
    len: usize,

    buffer: B,
}

Ain't clear if you could make the unsized type VecSlice<T,[MaybeUninit<T>]> works too, but if so maybe this breaks the len optimization. It also maters if SIMD breaks this optimization.

Some design patterns are completely reliant on using an existing buffer

Yes, but I'll caution this always felt more diverse than Vec in the past, like if everything is initialized then this trivial little function does amazing things:

pub fn reserve_mut<'heap, T>(heap: &mut &'heap mut [T], len: usize) -> &'heap mut [T] {
    let tmp: &'heap mut [T] = ::std::mem::replace(&mut *heap, &mut []);
    let (reserved, tmp) = tmp.split_at_mut(len);
    *heap = tmp;
    reserved
}

It's maybe cool then turning each of the resulting slices into Vec variants, so maybe it'd all play nicely.

@contactomorph
Copy link

contactomorph commented Oct 17, 2022

Am I the only one who thinks that ArrayVec is a terrible name? To me it obviously suggests a two-dimensional collection: a vector of arrays.

"FooBar" meaning "a Bar backed by Foo" has precedents in std: VecDeque (as well as BTreeMap &co.)

That does not make it a proof that this a good idea. Else one would deduce that once a mistake is done in std, it does not matter anymore. That said, my point is that it is generally not considered a great idea to name a type/function based on implementation details (so same criticism for VecDeque and BTreeMap)

Besides your point is not even comparing completely similar cases: At least a BTreeMap has the appropriate interface to be a Map. On the contrary it would not be reasonable ArrayVec to have the same interface as a Vec and the current RFC is showing that difference: most of the methods are indeed fallible. So to me the current type is not even really a Bar backed by a Foo.

@rursprung
Copy link

i might have missed it in the RFC, but what i'd really like is to have a trait which covers all alloc-independent methods (push, len, etc.) wich you'd expect from a Vec so that code can be written against this trait without knowing the underlying implementation.

that way more portable code can be written which either receives an implementation from the outside or only needs to #cfg-guard the instantiation of the vector. all the rest (including the use statements) could then stay the same.

in the best case that'd exist not just for Vec but also for other alloc structures where alternatives exist, e.g. in the heapless crate.

@tarcieri
Copy link

tarcieri commented Jul 2, 2023

@rursprung FWIW, the collectable crate tries to provide such a set of traits for fallible operations on container types, along with impls for Vec, heapless::Vec, and tinyvec::Array: https://docs.rs/collectable/

@rursprung
Copy link

@rursprung FWIW, the collectable crate tries to provide such a set of traits for fallible operations on container types, along with impls for Vec, heapless::Vec, and tinyvec::Array: https://docs.rs/collectable/

i might be missing something, but i neither see implementations in this crate nor APIs for most of the actions (e.g. push)? also, v0.0.2 released 3 years ago being the last release doesn't increase my confidence in the crate...

@tarcieri
Copy link

tarcieri commented Jul 2, 2023

push is TryPush::try_push, because all of the traits are intended to support fallible allocations.

It is indeed an experimental crate. I bring it up more as food for thought regarding gaps in current core traits for collection types backed by fallible allocators and various other common patterns. One of the main ones that comes to mind for me there is Extend, where TryExtend would be nice for ArrayVec-like types.

@tgross35
Copy link
Contributor Author

tgross35 commented Jul 2, 2023

Thanks all for reminding me about this PR.

Since the time this was drafted, there have been two things I am aware of that are likely better solutions than this proposal:

  • The storages proposa (just "store" now?) mentioned by @CAD97 is now a RFC
  • @pitaj has an awesome experiment where allocators trait contain a Result type and a conversion method, meaning that an allocator would be able to be fallible or infallible and an allocated type would be able to propegate its errors in a fallible way (so e.g. Vec<T, Global>::push(T) -> () but Vec<T, FallibleAlloc>::push(T) -> Result<(), AllocError>

I do think the ideas in this RFC have a place in core so I won't close this, but I think it's best to wait out one of the two things I linked to see what the best way to do that is (since it's likely that one of them will intrinsically give us a solution)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-libs-api Relevant to the library API team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.