How does smallvec::SmallVec::into_vec differ from into_inner for consuming the vector?
into_vec always converts a SmallVec into a heap-allocated Vec<T>, potentially copying data from inline storage to the heap, while into_inner attempts to extract the inline array directly and fails if the data has spilled to heap allocation. into_vec is the unconditional conversion path—guaranteed to succeed but may allocate and copy. into_inner is the zero-cost extraction path—succeeds only when the data fits within the inline capacity, avoiding any heap operations. Choosing between them depends on whether you prioritize guaranteed success (into_vec) or maximum performance when possible (into_inner).
The Two Consumption Paths
use smallvec::SmallVec;
fn two_paths() {
// SmallVec with inline capacity of 4
let small: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3]);
// into_vec: Always returns Vec, may allocate
let vec = small.into_vec();
assert_eq!(vec, vec![1, 2, 3]); // Vec<i32> on heap
// into_inner: Returns inline array only if not spilled
let small2: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3]);
let result = small2.into_inner();
// Succeeds because data fits inline
assert_eq!(result.unwrap(), [1, 2, 3, 0]); // Array of size 4
}into_vec guarantees a Vec; into_inner tries for zero-cost extraction.
into_vec: Unconditional Conversion
use smallvec::SmallVec;
fn into_vec_behavior() {
// Case 1: Data fits inline (stack)
let small: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3]);
let vec = small.into_vec();
// Copies data from stack to heap
// Allocates Vec on heap
assert_eq!(vec, vec![1, 2, 3]);
// Case 2: Data spilled to heap
let large: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3, 4, 5]);
let vec = large.into_vec();
// Data already on heap, reuses allocation
// No copy needed (or minimal copy)
assert_eq!(vec, vec![1, 2, 3, 4, 5]);
// into_vec always succeeds:
// Returns Vec<T> regardless of where data was stored
// Signature: fn into_vec(self) -> Vec<T>
}into_vec handles both inline and spilled data uniformly.
into_inner: Conditional Extraction
use smallvec::SmallVec;
fn into_inner_behavior() {
// Case 1: Data fits inline (succeeds)
let small: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3]);
let result = small.into_inner();
assert!(result.is_ok());
let array = result.unwrap();
assert_eq!(array, [1, 2, 3, 0]); // Array of capacity 4
// Case 2: Data spilled to heap (fails)
let large: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3, 4, 5]);
let result = large.into_inner();
assert!(result.is_err());
// Returns Err containing the original SmallVec
// Cannot extract inline array because data is on heap
// Signature: fn into_inner(self) -> Result<A, Self>
// where A is the array type (e.g., [i32; 4])
}into_inner succeeds only when data remained inline.
Memory Layout Implications
use smallvec::SmallVec;
fn memory_layout() {
// SmallVec<[T; N]> stores:
// - Inline: Array of N elements on stack
// - Spilled: Vec on heap (for >N elements)
let mut small: SmallVec<[i32; 4]> = SmallVec::new();
// Push elements within capacity
small.push(1); // Inline
small.push(2); // Inline
small.push(3); // Inline
// into_inner succeeds: data is inline
let array = small.into_inner().unwrap();
// No heap allocation occurred
// Array contains [1, 2, 3, 0]
// Now with spilling
let mut large: SmallVec<[i32; 2]> = SmallVec::new();
large.push(1); // Inline
large.push(2); // Inline (at capacity)
large.push(3); // Spills to heap!
// into_inner fails: data is on heap
let result = large.into_inner();
assert!(result.is_err());
// into_vec always works
let vec = result.unwrap_err().into_vec();
assert_eq!(vec, vec![1, 2, 3]);
}Spilling occurs when elements exceed inline capacity.
Allocation Behavior Comparison
use smallvec::SmallVec;
fn allocation_behavior() {
// into_vec allocation behavior:
//
// If data is inline (on stack):
// 1. Allocates heap memory for Vec
// 2. Copies data from stack to heap
// 3. Returns Vec pointing to heap
//
// If data is spilled (on heap):
// 1. Reuses existing heap allocation
// 2. Returns Vec wrapping same allocation
// 3. Minimal overhead
// into_inner allocation behavior:
//
// If data is inline:
// 1. Returns Ok(array)
// 2. Zero allocation
// 3. Zero copy
//
// If data is spilled:
// 1. Returns Err(SmallVec)
// 2. Zero allocation
// 3. Returns ownership unchanged
// Example: When to use which
// Use into_inner when you expect inline storage
// and want zero-cost extraction
// Use into_vec when you need Vec regardless
// of where data is stored
}into_inner avoids allocation when successful; into_vec may allocate.
Return Types
use smallvec::SmallVec;
fn return_types() {
let small: SmallVec<[String; 2]> = SmallVec::from_slice(&[
"a".to_string(),
"b".to_string(),
]);
// into_vec returns Vec<T>
let vec: Vec<String> = small.into_vec();
// Always Vec<String>, regardless of storage
let small2: SmallVec<[String; 2]> = SmallVec::from_slice(&[
"a".to_string(),
"b".to_string(),
]);
// into_inner returns Result<A, Self>
// where A is the array type
let result: Result<[String; 2], SmallVec<[String; 2]>> = small2.into_inner();
// Success case: Ok([String; N])
// Failure case: Err(SmallVec)
// The array type includes the capacity:
// SmallVec<[T; N]> -> into_inner returns Result<[T; N], Self>
}into_vec returns Vec<T>; into_inner returns Result<[T; N], Self>.
Handling into_inner Failure
use smallvec::SmallVec;
fn handling_failure() {
let maybe_inline: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3, 4, 5]);
// into_inner might fail - handle both cases
match maybe_inline.into_inner() {
Ok(array) => {
// Fast path: data was inline
println!("Inline: {:?}", array);
}
Err(smallvec) => {
// Data was on heap
// Can still convert to Vec if needed
let vec = smallvec.into_vec();
println!("Spilled: {:?}", vec);
}
}
// Common pattern: fallback to into_vec
let small: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3, 4, 5]);
let vec = small.into_inner()
.map(|arr| arr.to_vec()) // Convert array to Vec
.unwrap_or_else(|s| s.into_vec()); // Or use into_vec on error
// Or simpler: just use into_vec if you need Vec anyway
}When into_inner fails, you can fall back to into_vec.
Performance-Sensitive Code
use smallvec::SmallVec;
fn performance_sensitive() {
// Scenario: Hot path where allocation matters
fn process_smallvec(data: SmallVec<[u8; 64]>) -> Vec<u8> {
// If we expect most data to fit inline:
// Try into_inner first for zero-cost path
match data.into_inner() {
Ok(array) => {
// Fast path: inline, no allocation
// Convert only the used portion
let len = array.iter().position(|&b| b == 0).unwrap_or(64);
array[..len].to_vec()
}
Err(smallvec) => {
// Slow path: spilled, convert to Vec
smallvec.into_vec()
}
}
}
// If we always need Vec:
fn process_to_vec(data: SmallVec<[u8; 64]>) -> Vec<u8> {
// into_vec is simpler, same result
data.into_vec()
}
// The first approach only wins if most data is inline
// and you want to avoid allocation in that case
}Use into_inner when you want to optimize the inline case.
Working with the Inline Array
use smallvec::SmallVec;
fn working_with_array() {
let small: SmallVec<[i32; 8]> = SmallVec::from_slice(&[1, 2, 3]);
// into_inner gives the full capacity array
// Including uninitialized/zeroed elements beyond len
let array = small.into_inner().unwrap();
// array is [i32; 8] with values:
// [1, 2, 3, 0, 0, 0, 0, 0]
// (assuming Default::default() is 0)
// The original SmallVec tracked length 3
// But the array has capacity 8
// Be careful to track length separately!
// Common pattern: store length separately
let small2: SmallVec<[i32; 8]> = SmallVec::from_slice(&[1, 2, 3]);
let len = small2.len();
let array = small2.into_inner().unwrap();
// Now use only array[..len]
for &val in &array[..len] {
println!("{}", val);
}
// This is one reason into_vec is often simpler:
// Vec tracks its own length
}into_inner returns the full capacity array; track length separately.
Capacity and Spilling
use smallvec::SmallVec;
fn capacity_and_spilling() {
// SmallVec<[T; N]> has inline capacity N
// Data spills to heap when len > N
let mut vec: SmallVec<[i32; 3]> = SmallVec::new();
println!("Inline capacity: {}", vec.inline_capacity()); // 3
println!("Current capacity: {}", vec.capacity()); // 3
vec.push(1);
vec.push(2);
vec.push(3);
println!("Len: {}, still inline", vec.len());
vec.push(4); // Spills!
println!("Len: {}, now on heap", vec.len());
println!("Capacity: {} (heap allocated)", vec.capacity());
// At this point:
// into_inner would fail (data on heap)
// into_vec would succeed (heap -> heap)
// Creating new SmallVec that fits:
let fits: SmallVec<[i32; 10]> = SmallVec::from_slice(&[1, 2, 3, 4, 5]);
assert!(fits.into_inner().is_ok()); // Fits inline
let overflows: SmallVec<[i32; 2]> = SmallVec::from_slice(&[1, 2, 3]);
assert!(overflows.into_inner().is_err()); // Spilled
}Spilling depends on the inline capacity and actual element count.
Converting Back to SmallVec
use smallvec::SmallVec;
fn back_to_smallvec() {
// From Vec back to SmallVec
let vec = vec![1, 2, 3, 4, 5];
let small: SmallVec<[i32; 10]> = SmallVec::from_vec(vec);
// Data fits inline, into_inner works
assert!(small.clone().into_inner().is_ok());
// From Vec that overflows
let large_vec = vec![1, 2, 3, 4, 5];
let small2: SmallVec<[i32; 2]> = SmallVec::from_vec(large_vec);
// Data spilled, into_inner fails
assert!(small2.clone().into_inner().is_err());
// From array to SmallVec
let arr = [1, 2, 3, 4];
let small3: SmallVec<[i32; 4]> = SmallVec::from_buf(arr);
// into_inner returns the array
assert_eq!(small3.into_inner().unwrap(), [1, 2, 3, 4]);
}Conversion paths between SmallVec, Vec, and arrays.
Use Case: Zero-Allocation Processing
use smallvec::SmallVec;
fn zero_allocation_example() {
// Scenario: Process small buffers without heap allocation
fn process_buffer(buf: SmallVec<[u8; 64]>) -> u32 {
// Try inline path first
match buf.into_inner() {
Ok(array) => {
// Zero-allocation path
// Process directly from stack
let len = 64 - array.iter().rev().position(|&b| b != 0).unwrap_or(0);
array[..len].iter().map(|&b| b as u32).sum()
}
Err(smallvec) => {
// Heap allocation already happened
// Use into_vec or process inline
smallvec.iter().map(|&b| b as u32).sum()
}
}
}
// Actually, simpler pattern:
fn process_simple(buf: SmallVec<[u8; 64]>) -> u32 {
// If most cases fit inline, into_inner + into_vec
// is overhead compared to just iterating
// SmallVec derefs to slice, no conversion needed!
buf.iter().map(|&b| b as u32).sum()
}
// into_inner is useful when you need ownership of the array
// not just for avoiding allocation while iterating
}Often, just iterating over SmallVec avoids needing into_inner.
Use Case: API Boundary
use smallvec::SmallVec;
fn api_boundary() {
// Scenario: Function expects Vec, but you have SmallVec
fn expects_vec(data: Vec<i32>) -> i32 {
data.into_iter().sum()
}
let small: SmallVec<[i32; 8]> = SmallVec::from_slice(&[1, 2, 3, 4, 5]);
// into_vec converts to expected type
let result = expects_vec(small.into_vec());
// If the function could take SmallVec:
fn accepts_smallvec(data: SmallVec<[i32; 8]>) -> i32 {
data.into_iter().sum()
}
let small2: SmallVec<[i32; 8]> = SmallVec::from_slice(&[1, 2, 3, 4, 5]);
// into_inner might extract array for functions expecting arrays
fn expects_array(data: [i32; 8]) -> i32 {
data.into_iter().sum()
}
if let Ok(array) = small2.into_inner() {
expects_array(array);
}
}Use into_vec when interfacing with Vec-based APIs.
Real Example: Compiler Internals
use smallvec::SmallVec;
fn compiler_example() {
// SmallVec is commonly used in compilers
// where most lists are small but some are large
// Example: List of local variables in a function
// Most functions have < 8 locals, some have many
type VarList = SmallVec<[u32; 8]>; // Inline capacity 8
fn process_vars(vars: VarList) -> Vec<String> {
// Most cases: vars fit inline (8 or fewer)
// into_inner would succeed
// Some cases: vars spilled to heap (9+)
// into_inner would fail
// For this use case, just convert to Vec at the end
vars.into_iter()
.map(|v| format!("var_{}", v))
.collect()
}
// Example: Token buffer during lexing
type TokenBuf = SmallVec<[Token; 16]>;
#[derive(Clone)]
struct Token {
kind: TokenKind,
span: Span,
}
#[derive(Clone)]
struct Span {
lo: u32,
hi: u32,
}
#[derive(Clone)]
enum TokenKind {
Ident,
Literal,
Operator,
}
// Tokens accumulate during lexing
// Most expressions have < 16 tokens
// into_inner useful for zero-copy extraction
}Compilers use SmallVec for small-but-sometimes-large collections.
Comparison Summary
use smallvec::SmallVec;
fn comparison_summary() {
// into_vec:
// - Always succeeds
// - Returns Vec<T>
// - May allocate if inline
// - May copy if inline
// - Reuses heap if spilled
// - Simpler to use
// - Good for Vec-based APIs
// into_inner:
// - Succeeds only if inline
// - Returns Result<[T; N], Self>
// - Never allocates
// - Never copies (success case)
// - Returns full capacity array
// - Requires handling failure
// - Good for performance optimization
// When to use into_vec:
// - Need guaranteed Vec
// - Interfacing with Vec APIs
// - Simplicity preferred
// - Don't care about allocation
// When to use into_inner:
// - Optimizing inline case
// - Need the array specifically
// - Performance-critical path
// - Most data fits inline
let small: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3]);
// Simple: always works
let vec = small.into_vec();
let small2: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3]);
// Zero-copy, but may fail
match small2.into_inner() {
Ok(array) => println!("Array: {:?}", array),
Err(s) => println!("Spilled, convert: {:?}", s.into_vec()),
}
}Choose based on your success requirements and performance needs.
Synthesis
Quick reference:
use smallvec::SmallVec;
fn quick_reference() {
let inline: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3]);
let spilled: SmallVec<[i32; 2]> = SmallVec::from_slice(&[1, 2, 3]);
// into_vec: Always returns Vec
let vec1: Vec<i32> = inline.into_vec();
let vec2: Vec<i32> = spilled.into_vec();
// Both work, vec1 allocated (inline -> heap)
// vec2 reused heap (already on heap)
// into_inner: Returns Result<[T; N], Self>
let inlined: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3]);
let result1 = inlined.into_inner();
assert!(result1.is_ok()); // Data was inline
// Returns Ok([i32; 4])
let spilled2: SmallVec<[i32; 2]> = SmallVec::from_slice(&[1, 2, 3]);
let result2 = spilled2.into_inner();
assert!(result2.is_err()); // Data was on heap
// Returns Err(SmallVec)
// Pattern: Try zero-copy first, fallback to into_vec
let data: SmallVec<[i32; 4]> = SmallVec::from_slice(&[1, 2, 3]);
let owned = data.into_inner()
.map(|arr| arr.to_vec())
.unwrap_or_else(|s| s.into_vec());
// Key insight:
// - into_vec: Simple, guaranteed, may allocate
// - into_inner: Zero-copy if inline, fails if spilled
// - Use into_vec for simplicity, into_inner for performance
}Key insight: into_vec and into_inner represent two different ownership transfer strategies for SmallVec. into_vec unconditionally converts to Vec<T>, handling both inline (stack) and spilled (heap) storage transparently—it may allocate and copy if data was inline, or reuse existing heap allocation if spilled. into_inner attempts zero-cost extraction of the inline array, returning Ok([T; N]) when data fits inline and Err(Self) when spilled. Use into_vec when you need guaranteed conversion to Vec regardless of storage location. Use into_inner when optimizing for the common inline case and want to avoid any allocation or copy on success, falling back to into_vec or other handling when spilling occurred.
