Collections#
- Source:
Rust’s standard library provides collections similar to C++ STL containers, but with
a key difference: Rust collections integrate deeply with the ownership and borrowing
system. This integration prevents iterator invalidation bugs at compile time - a
common source of undefined behavior in C++. When you iterate over a Rust collection,
the borrow checker ensures you cannot accidentally modify the collection in ways that
would invalidate the iteration. Additionally, Rust collections are designed with
modern CPU architectures in mind, favoring cache-friendly data structures like
Vec and HashMap over pointer-heavy structures like linked lists.
The table below maps common C++ STL containers to their Rust equivalents. Note that
Rust’s HashMap and HashSet use a different hashing algorithm (SipHash by
default) than most C++ implementations, prioritizing security against hash flooding
attacks over raw speed.
C++ |
Rust |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Vec (Vector)#
Vec<T> is Rust’s growable array type, equivalent to C++’s std::vector. It
stores elements contiguously in heap memory and provides O(1) amortized push/pop
at the end. The vec! macro provides convenient initialization syntax similar
to C++’s initializer lists.
The following example demonstrates basic vector operations including creation,
modification, and iteration. Note that Rust requires explicit mutability with
let mut to modify the vector:
C++:
#include <iostream>
#include <vector>
int main() {
// Create and initialize a vector
std::vector<int> v = {1, 2, 3};
// Add and remove elements from the back
v.push_back(4);
v.push_back(5);
v.pop_back(); // removes 5
// Iterate and print elements
std::cout << "Elements: ";
for (int x : v) {
std::cout << x << " ";
}
std::cout << "\n";
// Access elements - unchecked vs bounds-checked
int first = v[0]; // no bounds check, UB if out of range
int safe = v.at(0); // throws std::out_of_range if invalid
std::cout << "First element: " << first << "\n";
std::cout << "Size: " << v.size() << "\n";
return 0;
}
Rust:
fn main() {
// Create and initialize a vector using vec! macro
let mut v = vec![1, 2, 3];
// Add and remove elements from the back
v.push(4);
v.push(5);
v.pop(); // removes 5, returns Some(5)
// Iterate and print elements (borrowing with &v)
print!("Elements: ");
for x in &v {
print!("{} ", x);
}
println!();
// Access elements - panics vs returns Option
let first = v[0]; // panics if out of bounds
let safe = v.get(0); // returns Option<&T>, None if invalid
println!("First element: {}", first);
println!("Safe access: {:?}", safe);
println!("Size: {}", v.len());
}
Vec Operations#
This example shows common vector operations including element access, modification,
searching, sorting, and capacity management. Rust’s iterator-based search methods
return Option types, making it explicit when an element might not be found:
C++:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
// Access elements
int first = v.front();
int last = v.back();
// Slice via iterators (no direct slice syntax)
// Modification
v.push_back(6);
v.pop_back();
v.insert(v.begin(), 0); // insert at front
v.erase(v.begin()); // remove from front
// Search
auto it = std::find(v.begin(), v.end(), 3);
bool found = (it != v.end());
size_t pos = std::distance(v.begin(), it);
// Sorting
std::sort(v.begin(), v.end());
std::sort(v.begin(), v.end(), std::greater<int>()); // reverse
// Capacity
size_t len = v.size();
size_t cap = v.capacity();
v.reserve(100);
v.shrink_to_fit();
std::cout << "Length: " << len << ", Capacity after reserve: " << v.capacity() << "\n";
return 0;
}
Rust:
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
// Access elements
let first = &v[0];
let last = v.last(); // Option<&T>
let slice = &v[1..3]; // [2, 3] - native slice syntax
println!("First: {}, Last: {:?}, Slice: {:?}", first, last, slice);
// Modification
v.push(6);
v.pop(); // returns Option<T>
v.insert(0, 0); // insert at index
v.remove(0); // remove at index, returns element
// Search - returns Option types for safety
let pos = v.iter().position(|&x| x == 3); // Option<usize>
let contains = v.contains(&3); // bool
println!("Position of 3: {:?}, Contains 3: {}", pos, contains);
// Sorting
v.sort();
v.sort_by(|a, b| b.cmp(a)); // reverse order
// Capacity management
let len = v.len();
let cap = v.capacity();
v.reserve(100);
v.shrink_to_fit();
println!("Length: {}, Capacity: {}", len, v.capacity());
}
HashMap#
HashMap<K, V> is Rust’s hash table implementation, equivalent to C++’s
std::unordered_map. Keys must implement the Hash and Eq traits. The
entry API provides an elegant way to handle insert-or-update patterns that
would require multiple lookups in C++.
This example demonstrates basic hash map operations including insertion, lookup,
and iteration. Note that Rust’s get method returns an Option, making it
impossible to accidentally use a non-existent key:
C++:
#include <iostream>
#include <string>
#include <unordered_map>
int main() {
std::unordered_map<std::string, int> map;
// Insert elements
map["one"] = 1;
map["two"] = 2;
map.insert({"three", 3});
// Lookup - must check if key exists
if (auto it = map.find("one"); it != map.end()) {
std::cout << "Found: " << it->second << "\n";
}
// Dangerous: creates entry if missing!
int val = map["four"]; // inserts {"four", 0}
// Iterate over key-value pairs
std::cout << "All entries:\n";
for (const auto& [key, value] : map) {
std::cout << " " << key << ": " << value << "\n";
}
std::cout << "Size: " << map.size() << "\n";
return 0;
}
Rust:
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
// Insert elements
map.insert("one", 1);
map.insert("two", 2);
map.insert("three", 3);
// Lookup - returns Option, no accidental insertion
if let Some(value) = map.get("one") {
println!("Found: {}", value);
}
// Safe: get returns None for missing keys
let missing = map.get("four");
println!("Missing key: {:?}", missing); // None
// Iterate over key-value pairs
println!("All entries:");
for (key, value) in &map {
println!(" {}: {}", key, value);
}
println!("Size: {}", map.len());
}
HashMap Entry API#
The entry API is one of Rust’s most powerful collection features. It allows you to inspect and modify map entries in a single lookup, avoiding the double-lookup pattern common in C++. This is particularly useful for counting, caching, and accumulation patterns:
C++:
#include <iostream>
#include <string>
#include <unordered_map>
int main() {
std::unordered_map<std::string, int> word_count;
std::string words[] = {"apple", "banana", "apple", "cherry", "banana", "apple"};
// Count words - requires lookup then insert/update
for (const auto& word : words) {
auto it = word_count.find(word);
if (it != word_count.end()) {
it->second++;
} else {
word_count[word] = 1;
}
}
// Or use operator[] which default-constructs if missing
std::unordered_map<std::string, int> word_count2;
for (const auto& word : words) {
word_count2[word]++; // relies on int default being 0
}
for (const auto& [word, count] : word_count) {
std::cout << word << ": " << count << "\n";
}
return 0;
}
Rust:
use std::collections::HashMap;
fn main() {
let mut word_count = HashMap::new();
let words = ["apple", "banana", "apple", "cherry", "banana", "apple"];
// Count words using entry API - single lookup!
for word in &words {
// or_insert returns &mut V, allowing in-place modification
*word_count.entry(*word).or_insert(0) += 1;
}
// Entry API variants:
let mut map: HashMap<&str, Vec<i32>> = HashMap::new();
// or_insert: insert value if missing
map.entry("numbers").or_insert(vec![]).push(1);
// or_insert_with: insert computed value if missing (lazy)
map.entry("computed").or_insert_with(|| {
println!("Computing default...");
vec![42]
});
// or_default: insert Default::default() if missing
map.entry("default").or_default().push(100);
// Print word counts
println!("Word counts:");
for (word, count) in &word_count {
println!(" {}: {}", word, count);
}
}
BTreeMap (Ordered Map)#
BTreeMap<K, V> is Rust’s ordered map, equivalent to C++’s std::map. It keeps
keys in sorted order and provides O(log n) operations. Unlike HashMap, keys only
need to implement Ord (not Hash). BTreeMap also supports efficient range
queries, which are not available with hash-based maps:
C++:
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<int, std::string> map;
// Insert in arbitrary order
map[3] = "three";
map[1] = "one";
map[2] = "two";
map[5] = "five";
map[4] = "four";
// Iteration is in sorted key order
std::cout << "All entries (sorted):\n";
for (const auto& [key, value] : map) {
std::cout << " " << key << ": " << value << "\n";
}
// Range query using lower_bound/upper_bound
std::cout << "Range [2, 4):\n";
for (auto it = map.lower_bound(2); it != map.lower_bound(4); ++it) {
std::cout << " " << it->first << ": " << it->second << "\n";
}
return 0;
}
Rust:
use std::collections::BTreeMap;
fn main() {
let mut map = BTreeMap::new();
// Insert in arbitrary order
map.insert(3, "three");
map.insert(1, "one");
map.insert(2, "two");
map.insert(5, "five");
map.insert(4, "four");
// Iteration is in sorted key order
println!("All entries (sorted):");
for (k, v) in &map {
println!(" {}: {}", k, v);
}
// Range queries - more ergonomic than C++
println!("Range [2, 4):");
for (k, v) in map.range(2..4) {
println!(" {}: {}", k, v);
}
// Other range variants
println!("Keys >= 3:");
for (k, v) in map.range(3..) {
println!(" {}: {}", k, v);
}
}
HashSet#
HashSet<T> is Rust’s hash set implementation, equivalent to C++’s
std::unordered_set. It stores unique values and provides O(1) average-case
lookup, insertion, and removal. Rust’s HashSet provides convenient methods for
set-theoretic operations like union, intersection, and difference:
C++:
#include <iostream>
#include <unordered_set>
#include <algorithm>
#include <iterator>
int main() {
std::unordered_set<int> set = {1, 2, 3};
// Insert and check membership
set.insert(4);
set.insert(2); // duplicate, ignored
bool has_two = set.count(2) > 0;
std::cout << "Contains 2: " << (has_two ? "yes" : "no") << "\n";
std::cout << "Size: " << set.size() << "\n";
// Set operations require manual implementation or std::set_*
std::unordered_set<int> other = {3, 4, 5};
// No built-in union/intersection for unordered_set
// Must convert to sorted containers or implement manually
return 0;
}
Rust:
use std::collections::HashSet;
fn main() {
let mut set: HashSet<i32> = [1, 2, 3].into();
// Insert and check membership
set.insert(4);
set.insert(2); // duplicate, returns false
let has_two = set.contains(&2);
println!("Contains 2: {}", has_two);
println!("Size: {}", set.len());
// Iterate (order is arbitrary)
print!("Elements: ");
for x in &set {
print!("{} ", x);
}
println!();
}
Set Operations#
Rust provides built-in methods for common set operations. These methods return iterators, allowing lazy evaluation and chaining. The following example demonstrates union, intersection, difference, and symmetric difference:
C++:
#include <algorithm>
#include <iostream>
#include <set> // using ordered set for set operations
#include <iterator>
int main() {
std::set<int> a = {1, 2, 3};
std::set<int> b = {2, 3, 4};
// Union
std::set<int> union_set;
std::set_union(a.begin(), a.end(), b.begin(), b.end(),
std::inserter(union_set, union_set.begin()));
// Intersection
std::set<int> intersection;
std::set_intersection(a.begin(), a.end(), b.begin(), b.end(),
std::inserter(intersection, intersection.begin()));
// Difference (a - b)
std::set<int> difference;
std::set_difference(a.begin(), a.end(), b.begin(), b.end(),
std::inserter(difference, difference.begin()));
// Symmetric difference
std::set<int> sym_diff;
std::set_symmetric_difference(a.begin(), a.end(), b.begin(), b.end(),
std::inserter(sym_diff, sym_diff.begin()));
auto print_set = [](const std::string& name, const std::set<int>& s) {
std::cout << name << ": {";
for (auto it = s.begin(); it != s.end(); ++it) {
if (it != s.begin()) std::cout << ", ";
std::cout << *it;
}
std::cout << "}\n";
};
print_set("Union", union_set);
print_set("Intersection", intersection);
print_set("Difference", difference);
print_set("Symmetric Diff", sym_diff);
return 0;
}
Rust:
use std::collections::HashSet;
fn main() {
let a: HashSet<i32> = [1, 2, 3].into();
let b: HashSet<i32> = [2, 3, 4].into();
// Set operations return iterators - collect into HashSet
let union_set: HashSet<_> = a.union(&b).cloned().collect();
let intersection: HashSet<_> = a.intersection(&b).cloned().collect();
let difference: HashSet<_> = a.difference(&b).cloned().collect();
let sym_diff: HashSet<_> = a.symmetric_difference(&b).cloned().collect();
println!("Union: {:?}", union_set); // {1, 2, 3, 4}
println!("Intersection: {:?}", intersection); // {2, 3}
println!("Difference: {:?}", difference); // {1}
println!("Symmetric Diff: {:?}", sym_diff); // {1, 4}
// Predicates for set relationships
let subset: HashSet<i32> = [2, 3].into();
println!("Is {{2,3}} subset of a? {}", subset.is_subset(&a));
println!("Are a and b disjoint? {}", a.is_disjoint(&b));
}
VecDeque (Double-ended Queue)#
VecDeque<T> is Rust’s double-ended queue, equivalent to C++’s std::deque.
It provides O(1) push and pop operations at both ends, making it ideal for
queue and sliding window implementations. Unlike Vec, it uses a ring buffer
internally:
C++:
#include <deque>
#include <iostream>
int main() {
std::deque<int> dq = {2, 3, 4};
// Push to both ends
dq.push_front(1);
dq.push_back(5);
// Pop from both ends
dq.pop_front();
dq.pop_back();
// Access elements
std::cout << "Front: " << dq.front() << "\n";
std::cout << "Back: " << dq.back() << "\n";
// Random access
std::cout << "Element at index 1: " << dq[1] << "\n";
// Iterate
std::cout << "Elements: ";
for (int x : dq) {
std::cout << x << " ";
}
std::cout << "\n";
return 0;
}
Rust:
use std::collections::VecDeque;
fn main() {
let mut dq = VecDeque::from([2, 3, 4]);
// Push to both ends
dq.push_front(1);
dq.push_back(5);
// Pop from both ends - returns Option<T>
let front = dq.pop_front(); // Some(1)
let back = dq.pop_back(); // Some(5)
println!("Popped front: {:?}, back: {:?}", front, back);
// Access elements
println!("Front: {:?}", dq.front()); // Option<&T>
println!("Back: {:?}", dq.back());
// Random access
println!("Element at index 1: {:?}", dq.get(1));
// Iterate
print!("Elements: ");
for x in &dq {
print!("{} ", x);
}
println!();
}
Iterator Invalidation#
One of Rust’s most significant safety improvements over C++ is compile-time prevention of iterator invalidation. In C++, modifying a collection while iterating over it leads to undefined behavior that may silently corrupt data or crash. Rust’s borrow checker makes this a compile-time error:
C++ (compiles but has undefined behavior):
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
// DANGEROUS: modifying vector while iterating
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 3) {
v.erase(it); // iterator invalidated! UB!
// Even "fixing" with: it = v.erase(it); --it;
// is error-prone
}
}
// This might crash, print garbage, or appear to work
for (int x : v) {
std::cout << x << " ";
}
return 0;
}
Rust (compile error prevents the bug):
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
// This won't compile - borrow checker prevents it
// for x in &v {
// if *x == 3 {
// v.remove(2); // error: cannot borrow `v` as mutable
// } // because it's borrowed as immutable
// }
// Correct approach 1: use retain (most idiomatic)
v.retain(|&x| x != 3);
println!("After retain: {:?}", v);
// Correct approach 2: collect indices first, remove in reverse
let mut v2 = vec![1, 2, 3, 4, 5];
let indices: Vec<_> = v2.iter()
.enumerate()
.filter(|(_, &x)| x == 3)
.map(|(i, _)| i)
.collect();
for i in indices.into_iter().rev() {
v2.remove(i);
}
println!("After index removal: {:?}", v2);
// Correct approach 3: drain_filter (nightly) or filter + collect
let v3 = vec![1, 2, 3, 4, 5];
let v3: Vec<_> = v3.into_iter().filter(|&x| x != 3).collect();
println!("After filter: {:?}", v3);
}
See Also#
Iterators - Iterator adapters and patterns
Rust Basics - Ownership with collections