Nugget 16

Home

The vec! Macro

vec![1, 2, 3] looks like a function call but has a !. That's your clue: it's a macro, doing compile-time magic that no regular function could do.

The Core Difference

// Vec::new() — creates EMPTY Vec only
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);  // Three statements!

// vec! macro — creates AND populates in one go
let v = vec![1, 2, 3];  // One expression!

That's it in a nutshell. But why a macro? Why not a function Vec::from_elements(&[1, 2, 3])?

What vec! Actually Does

At compile time, vec![1, 2, 3] expands to something like this:

// What you write:
let v = vec![1, 2, 3];

// What the compiler generates:
let v = {
    let mut v = Vec::with_capacity(3);  // Pre-allocates!
    v.push(1);
    v.push(2);
    v.push(3);
    v
};

The macro knows the element count at compile time, so it calls with_capacity — one allocation, no reallocation.

Three Things vec! Can Do

1. Initialize with values directly

let v = vec![1, 2, 3, 4, 5];  // ✅ Clear, concise

let v = Vec::new();  // Empty — must push in a loop

2. Create repeated values (vec![val; count])

let zeros = vec![0; 100];      // ✅ 100 zeros, one expression
let falses = vec![false; 50];  // ✅ 50 false values

// Without the macro: loop with 100 pushes
let mut zeros = Vec::with_capacity(100);
for _ in 0..100 { zeros.push(0); }

3. Works with any copyable or cloneable type

#[derive(Clone)]
struct Point { x: i32, y: i32 }

let points = vec![
    Point { x: 0, y: 0 },
    Point { x: 1, y: 0 },
    Point { x: 0, y: 1 },
];  // ✅ Works fine — values provided explicitly

let same = vec![Point { x: 0, y: 0 }; 3];  // ✅ Repeats — needs Clone

Why a Macro Instead of a Function?

Rust could have designed Vec with static methods:

// Hypothetical (doesn't exist):
Vec::from_elements(&[1, 2, 3]);  // From slice
Vec::from_repeated(0, 100);     // Repeat value

// But these are more verbose and less flexible.

Rust chose macros over variadic functions (functions with variable arguments). Variadic functions complicate the type system — how do you type-check vec(1, 2, 3) vs vec(1, 2, 3, 4) with different lengths? A macro generates code at compile time, so there's no type-system headache.

// Other languages handle this differently:
// JavaScript: [1, 2, 3] — array literal, built into the language
// C: int arr[] = {1, 2, 3}; — also a language feature, not a library
// Python: [1, 2, 3] — list literal, syntax-level

// Rust: vec![1, 2, 3] — a library macro, not language syntax

The ! is your clue: compile-time code generation is happening. Rust keeps the language core small and pushes convenience into macros.

What vec! Can't Do

The macro is for when you know the values at compile time. For dynamic data, use iterators:

// ❌ This doesn't create vec![0, 1, 2, 3, 4]
let v = vec![0..5];  // Creates vec![Range] — a Vec with one Range element!

// ✅ Use collect for dynamic ranges
let v: Vec<i32> = (0..5).collect();   // [0, 1, 2, 3, 4]

// ✅ Use collect for transformation
let v: Vec<i32> = (0..5).map(|x| x * 10).collect();  // [0, 10, 20, 30, 40]

💡 vec! vs collect()

When you know the elements: vec![1, 2, 3]. When you're transforming or generating: (0..5).map(f).collect(). They complement each other — the macro for literals, iterators for computed sequences.

Performance

The macro knows the element count at compile time, so it pre-allocates the exact capacity:

// vec![1, 2, 3] generates:
let mut v = Vec::with_capacity(3);  // Exact size known at compile time
v.push(1);
v.push(2);
v.push(3);
// No reallocations — exactly one heap allocation of the right size.

// Compare with dynamic building:
let mut v = Vec::new();
v.push(1);  // May allocate (capacity = some default)
v.push(2);  // May reallocate
v.push(3);  // May reallocate again
// Up to 3 allocations instead of 1.

So vec! isn't just convenient — it's also the most efficient way to create a Vec from known elements.

Key Takeaways