Introduction
When you open the Rust standard library documentation to view the definition of <span>Vec<T></span>, have you noticed that mysterious line <span>{ /* private fields */ }</span>? As Rust developers, we use Vec every day, but what exactly is hidden inside?
Today, let’s dive deep into the source code of the Rust standard library, peeling back the abstract layers of <span>Vec<T></span> to see how Rust engineers have constructed a sophisticated type system from the bare pointers at the lowest level to the safe and user-friendly API.
The Appearance and Reality of Vec
Definition in Documentation
In the Rust API documentation, the definition of <span>Vec<T></span> looks like this:
pub struct Vec<T, A = Global>
where
A: Allocator,
{ /* private fields */ }
The documentation only provides a visual representation of the data structure:
ptr len capacity
+--------+--------+--------+
| 0x0123 | 2 | 4 |
+--------+--------+--------+
|
v
Heap +--------+--------+--------+--------+
| 'a' | 'b' | uninit | uninit |
+--------+--------+--------+--------+
We intuitively think that Vec should have three fields: <span>ptr</span>, <span>len</span>, and <span>capacity</span>. But is that really the case?
The Real Definition in Source Code
When we delve into the source code of <span>std::vec</span>, we find the following definition:
pub struct Vec<T, A: Allocator = Global> {
buf: RawVec<T, A>, // Buffer management
len: usize, // Number of initialized elements
}
Alright, we found the <span>len</span> field! But where did <span>ptr</span> and <span>capacity</span> go? They are hidden within the type <span>RawVec<T, A></span>. More interestingly, when you search for <span>RawVec</span> in the documentation, you find nothing—because it is defined as <span>pub(crate)</span>, visible only within the crate.
A Journey of Layered Abstraction
Let’s explore the complete structure of Vec, layer by layer, starting from the bare pointer like climbing a mountain.
First Layer: Bare Pointer <span>*const u8</span> – The Foundation
At the lowest level, we find the most primitive representation of a memory address in Rust:
// The lowest level bare pointer
*const u8
This is a raw memory address, just a number. It:
- Can be a null pointer
- Can point to freed memory (dangling pointer)
- Can be unaligned
- Has no lifecycle information
Using it must be within an <span>unsafe</span> block, telling the compiler: “I know what I’m doing.” This is a necessary starting point because to manage memory, you must first be able to directly manipulate memory addresses.
Important Note: Why is the pointer type <span>u8</span> instead of <span>usize</span>?
- The value (address) of the pointer is based on
<span>usize</span> - But the pointer points to a single byte in memory (
<span>u8</span>) - This is the starting point of the data, and at this moment we don’t care about the specific type, just a block of expandable memory buffer
Second Layer: <span>NonNull<u8></span> – Non-null Guarantee
pub struct NonNull<T: PointeeSized> {
pointer: *const T, // Internally wraps a bare pointer
}
<span>NonNull<u8></span> is the first layer of safe wrapping, providing a crucial guarantee: The pointer is never null.
This simple guarantee brings two significant benefits:
- Elimination of null pointer errors: From the type system level, it eradicates the “billion-dollar mistake” (null pointer exception)
- Zero-cost optimization: The compiler knows that
<span>NonNull<T></span>is never null, so<span>Option<NonNull<T>></span>can use a null address to represent<span>None</span>, occupying the same space as a bare pointer
Example:
use std::ptr::NonNull;
// Create a non-null pointer
let mut x = 42;
let ptr = NonNull::new(&mut x as *mut i32).unwrap();
// The size of Option<NonNull<T>> is the same as a bare pointer
assert_eq!(
std::mem::size_of::<Option<NonNull<i32>>>(),
std::mem::size_of::<*const i32>()
);
Third Layer: <span>Unique<u8></span> – Exclusive Ownership
pub struct Unique<T: PointeeSized> {
pointer: NonNull<T>, // Non-null pointer
_marker: PhantomData<T>, // Phantom data for type marking
}
<span>Unique<T></span> builds upon <span>NonNull</span> by adding an important concept: Ownership Signal.
It conveys two pieces of information to the compiler:
- “I am the sole owner of this data”
- “I am responsible for releasing (Drop) this data when it goes out of scope”
Key Understanding: <span>Unique<T></span> is not a guard enforcing ownership, but a “marker”. Creating a <span>Unique<T></span><code><span> is safe because merely creating it does not lead to undefined behavior. The real safety boundary lies in</span> <strong><span>accessing the data</span></strong><span>.</span>
// Creating a Unique pointer is safe
let unique_ptr = Unique::new_unchecked(some_ptr);
// But accessing data requires unsafe
unsafe {
let value = unique_ptr.as_ref(); // You must guarantee no other references exist
}
When you call <span>as_mut()</span> or <span>as_ref()</span> to access memory, you must enter an <span>unsafe</span> block and commit: “I guarantee no other pointers are accessing this memory.” By adhering to this commitment, you gain compile-time borrow checking and data race protection.
Fourth Layer: <span>RawVecInner<A></span> – Capacity Management
struct RawVecInner<A: Allocator = Global> {
ptr: Unique<u8>, // Exclusive pointer
cap: Cap, // Capacity management (boundary checks)
alloc: A, // Memory allocator
}
At this layer, we finally see the shadow of <span>capacity</span> (in the form of <span>Cap</span>)! <span>RawVecInner</span> is responsible for managing:
- The capacity of the memory (
<span>cap</span>) - The memory allocator (
<span>alloc</span>)
<span>Cap</span> type is specifically used to manage the minimum and maximum boundary of capacity, preventing overflow.
Fifth Layer: <span>RawVec<T, A></span> – Memory Allocation
pub(crate) struct RawVec<T, A: Allocator = Global> {
inner: RawVecInner<A>, // Internal implementation
_marker: PhantomData<T>, // Type marker
}
<span>RawVec<T></span> is the “low-level tool for managing memory buffers”. It is responsible for:
- Communicating with the memory allocator
- Allocating memory blocks on the heap
- Increasing capacity when needed (usually doubling)
- Releasing memory when Vec is destroyed
Important Feature: <span>RawVec</span> only knows the capacity (total allocated space) and does not track the number of initialized elements. This separation of concerns makes it a reusable building block—other collection types in the standard library, such as <span>VecDeque<T></span>, also use it.
Compilation Optimization Tip: Separating the logic into <span>RawVec</span> and <span>RawVecInner</span> ensures that the part not dependent on the generic <span>T</span> (<span>RawVecInner</span>) is not recompiled for each <span>Vec<T></span>, speeding up compilation.
Sixth Layer: <span>Vec<T></span> – Complete Safe API
pub struct Vec<T, A: Allocator = Global> {
buf: RawVec<T, A>, // Memory management
len: usize, // Number of initialized elements
}
Finally, we reach the summit! <span>Vec<T></span> integrates all parts:
- Holds
<span>RawVec</span>(managing memory) - Adds the final piece of the puzzle:
<span>len</span>(number of initialized elements)
<span>Vec</span> knows how many elements are initialized within the allocated block (capacity). Its responsibility is to ensure you can only access initialized elements, providing the safe methods we love in our daily work: <span>push</span>, <span>pop</span>, <span>insert</span>, indexing, etc.
The Complete Stack of Abstraction
Let’s review the entire type stack:
Vec<T> holds
↓
RawVec<T> holds
↓
RawVecInner holds
↓
Unique<u8> holds
↓
NonNull<u8> holds
↓
*const u8 (bare pointer)
Each layer builds upon the previous one, adding new guarantees and responsibilities until we have a completely safe, efficient, and powerful data structure.
Practical Case: Understanding the push Operation
What happens behind the scenes when you execute <span>vec.push(element)</span>?
let mut vec = Vec::new();
vec.push(42);
The layered calls behind this simple operation:
- Vec Layer: Checks
<span>len < capacity</span> - RawVec Layer: If capacity is insufficient, calls the allocator to grow memory
- Unique/NonNull Layer: Obtains a pointer to the write location
- Bare Pointer Layer: Actually writes to memory
- Vec Layer: Increases
<span>len</span>
Each layer fulfills its own responsibilities, collectively ensuring the safety and efficiency of the operation.
A Paradigm of Zero-Cost Abstraction
Rust’s Vec perfectly embodies the concept of “zero-cost abstraction”:
// High-level safe API
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
// After compilation, performance is equivalent to handwritten memory management code
// But you don’t need to write unsafe code
All these layers of abstraction are optimized away at compile time, resulting in machine code that performs comparably to your handwritten low-level C code, but you gain complete memory safety guarantees.
Further Thoughts
There are several topics worth exploring further in this article:
- How does NonNull guarantee its non-null promise?
- How does RawVec manage memory growth strategies?
- What is the role of PhantomData?
- How does the Allocator trait work?
Each type deserves its own article, and I recommend using your IDE’s “go to definition” feature to explore the implementations of these types. Rust’s documentation is very detailed, and the source code is clearly written.
Conclusion
By delving into the internal structure of Vec, we have unveiled the mystery behind those “private fields”. Starting from the bare pointer <span>*const u8</span>, through <span>NonNull</span> (non-null guarantee), <span>Unique</span> (ownership marker), <span>RawVecInner</span> (capacity management), <span>RawVec</span> (memory allocation), and finally reaching <span>Vec</span> (safe API), each layer precisely adds a guarantee or responsibility.
This is not a conspiracy, but excellent engineering design. Through layered abstraction and separation of concerns, the Rust standard library constructs data structures that are both safe and efficient. Each layer can be independently understood and reused, forming a powerful and flexible type system.
The next time you use <span>vec.push()</span>, you will know that a whole set of ingenious abstraction mechanisms is safeguarding you—this in itself is a cool secret.
Reference Articles
- Under the hood: Vec: https://marma.dev/articles/2025/under-the-hood-vec-t
Book Recommendations
The second edition of the book “The Rust Programming Language” is an authoritative learning resource written by the Rust core development team and translated by members of the Chinese Rust community. It is suitable for all software developers who wish to evaluate, get started, improve, and study the Rust language, and is regarded as essential reading for Rust development work.
This book introduces the fundamental concepts of the Rust language to unique practical tools, covering advanced concepts such as ownership, traits, lifetimes, and safety guarantees, as well as practical tools like pattern matching, error handling, package management, functional features, and concurrency mechanisms. The book includes three complete project development case studies, guiding readers to develop Rust practical projects from scratch.
Notably, this book has been updated to the Rust 2021 version, meeting the systematic learning needs of beginners and serving as a reference guide for experienced developers, making it the best entry point for building solid Rust skills.
Recommended Reading
-
Rust: The Performance King Sweeping C/C++/Go?
-
A C++ Perspective from Rust Developers: Revealing Pros and Cons
-
Rust vs Zig: The Battle of Emerging System Programming Languages
-
Essential Design Patterns for Asynchronous Programming in Rust: Enhancing Your Code Performance and Maintainability