Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,89 @@ impl<T, const N: usize, A: Allocator> From<SmallVec<T, N, A>> for Vec<T> {
}
}

// Without the allocator API, `Box` is always `Global`. The conversion still
// has to move elements out of inline storage; heap storage is rebuilt by
// `Vec`, which is also `Global`.
#[cfg(not(feature = "allocator-api"))]
impl<T, const N: usize, A: Allocator> From<SmallVec<T, N, A>> for Box<[T]> {
fn from(this: SmallVec<T, N, A>) -> Self {
Vec::from(this).into_boxed_slice()
}
}

// Preserve `A` instead of rebuilding through `Vec` (which is `Global`).
// Elements are copied into a new allocation owned by `A`. Spilled storage is
// then released with that same allocator, so the original buffer is not left
// to `Vec`'s global deallocator.
#[cfg(all(feature = "allocator-api", not(feature = "allocator-api2")))]
impl<T, const N: usize, A> From<SmallVec<T, N, A>> for Box<[T], A>
where A: Allocator + core::alloc::Allocator
{
fn from(this: SmallVec<T, N, A>) -> Self {
into_boxed_with_allocator(this)
}
}

#[cfg(feature = "allocator-api2")]
impl<T, const N: usize, A> From<SmallVec<T, N, A>> for Box<[T], A>
where A: Allocator + allocator_api2::alloc::Allocator
{
fn from(this: SmallVec<T, N, A>) -> Self {
into_boxed_with_allocator(this)
}
}

#[cfg(feature = "allocator-api2")]
use allocator_api2::alloc::Allocator as BoxAlloc;
#[cfg(all(feature = "allocator-api", not(feature = "allocator-api2")))]
use core::alloc::Allocator as BoxAlloc;

#[cfg(feature = "allocator-api")]
fn into_boxed_with_allocator<T, const N: usize, A>(this: SmallVec<T, N, A>) -> Box<[T], A>
where A: Allocator + BoxAlloc {
use core::{
alloc::Layout,
mem::{
MaybeUninit,
size_of
},
ptr::{
NonNull,
read
}
};

let this = ManuallyDrop::new(this);
let (length, on_heap) = this.length.parts();
let source = unsafe { this.raw.as_ptr(on_heap) };
let allocator = unsafe { read(&this.allocator) };

let mut boxed = Box::<[MaybeUninit<T>], A>::new_uninit_slice_in(length, allocator);
unsafe {
copy_nonoverlapping(source, boxed.as_mut_ptr().cast(), length);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This copy/realloc can be avoided if on_heap && length == capacity. In that case, this whole function could just be Vec::from(this).into_boxed_slice() again (once I finish implementing #702).

@alejandro-vaz alejandro-vaz Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

but we can remove the intermediate Vec conversion

why would we want it there

I'm not sure but probably our desired behavior is to always have it on the heap when converting it from a box

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

but we can remove the intermediate Vec conversion

Is there any overhead to that conversion? It seems like that would do the exact same things that a manual implementation would do.

I'm not sure but probably our desired behavior is to always have it on the heap when converting it from a box

This is converting to a box, not from.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

oops, sorry

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is there any overhead to that conversion? It seems like that would do the exact same things that a manual implementation would do.

yeah it's basically the same now that I think about it

after optimizations it probably compiles to the same thing

}

if on_heap && size_of::<T>() != 0 {
let (ptr, capacity) = unsafe { this.raw.heap };
if capacity != 0 {
let layout = unsafe {
Layout::from_size_align_unchecked(
capacity * size_of::<T>(),
core::mem::align_of::<T>()
)
};
unsafe {
Allocator::deallocate(
&this.allocator,
NonNull::new_unchecked(ptr.as_ptr().cast()),
layout
);
}
}
}

let (ptr, allocator) = Box::into_raw_with_allocator(boxed);
let slice = core::ptr::slice_from_raw_parts_mut(ptr.cast(), length);
// SAFETY: every element was copied from an initialized `SmallVec`.
unsafe { Box::from_raw_in(slice, allocator) }
}
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -987,7 +987,9 @@ impl<T, const N: usize, A: Allocator> SmallVec<T, N, A> {
#[inline]
#[deprecated(since = "2.0.0", note = "use `Into::<Box<[T]>>::into` instead")]
pub fn into_boxed_slice(self) -> Box<[T]> {
self.into()
// `From` for `Box<[T], A>` is not always `Box<[T]>` (the `Global`
// box). Keep this deprecated helper on the global allocator.
Vec::from(self).into_boxed_slice()
}

#[inline]
Expand Down
18 changes: 18 additions & 0 deletions tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1143,3 +1143,21 @@ fn spare_capacity_mut() {
assert!(!spare.is_empty());
assert_eq!(spare.as_ptr().cast::<u8>(), unsafe { v.as_ptr().add(3) });
}

#[test]
fn into_box_from_inline_and_spilled() {
let inline: SmallVec<u32, 8> = SmallVec::from([1, 2, 3]);
assert!(!inline.spilled());
let boxed: Box<[u32]> = inline.into();
assert_eq!(&*boxed, &[1, 2, 3]);

let mut spilled: SmallVec<u32, 2> = SmallVec::new();
spilled.extend(1..6);
assert!(spilled.spilled());
let boxed: Box<[u32]> = spilled.into();
assert_eq!(&*boxed, &[1, 2, 3, 4, 5]);

let empty: SmallVec<u32, 4> = SmallVec::new();
let boxed: Box<[u32]> = empty.into();
assert!(boxed.is_empty());
}
Loading