Project 4: Generic Container Library
Build STL-like implementations of vector, list, and unordered_map with proper iterators, allocator support, and exception safety.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Advanced |
| Time Estimate | 3-4 weeks |
| Language | C++ |
| Prerequisites | Projects 1-3, template basics, understanding of complexity |
| Key Topics | Templates, iterators, memory allocation, exception safety, move semantics |
1. Learning Objectives
After completing this project, you will:
- Master C++ template class design and implementation
- Understand iterator categories and how to implement them
- Learn memory allocation strategies (growth factors, allocators)
- Implement exception safety guarantees (basic, strong, nothrow)
- Apply move semantics for efficient container operations
- Understand why the STL is designed the way it is
2. Theoretical Foundation
2.1 Core Concepts
Template Classes
Templates let you write generic code that works with any type:
template<typename T>
class MyVector {
T* data_;
size_t size_;
// Works with int, double, std::string, any type T
};
The compiler generates a new class for each type used:
MyVector<int>is a separate class fromMyVector<double>- This is called template instantiation
Iterator Design
Iterators are the glue between containers and algorithms. The STL defines iterator categories:
| Category | Operations | Examples |
|---|---|---|
| Input | ++, *, == | istream_iterator |
| Output | ++, * | ostream_iterator |
| Forward | Input + multi-pass | forward_list::iterator |
| Bidirectional | Forward + – | list::iterator |
| Random Access | Bidirectional + +n, -n, [] | vector::iterator |
For vector, the iterator is just a pointer (random access). For list, the iterator wraps a node pointer (bidirectional).
// Random access iterator for vector
template<typename T>
class VectorIterator {
T* ptr_;
public:
T& operator*() { return *ptr_; }
VectorIterator& operator++() { ++ptr_; return *this; }
VectorIterator operator+(ptrdiff_t n) { return {ptr_ + n}; }
bool operator==(const VectorIterator& other) { return ptr_ == other.ptr_; }
// ... more operations
};
Allocators
Allocators abstract memory management:
template<typename T, typename Allocator = std::allocator<T>>
class MyVector {
Allocator alloc_;
T* allocate(size_t n) {
return std::allocator_traits<Allocator>::allocate(alloc_, n);
}
void construct(T* p, const T& value) {
std::allocator_traits<Allocator>::construct(alloc_, p, value);
}
};
Why allocators?
- Custom memory pools
- Arena allocators for games
- Shared memory allocators
- Debug allocators that track allocations
Exception Safety Guarantees
| Guarantee | Meaning | Example |
|---|---|---|
| Nothrow | Never throws | swap(), destructors |
| Strong | If exception, state unchanged | push_back() (usually) |
| Basic | No leaks, valid (but changed) state | insert() range |
For push_back, strong guarantee means:
- If reallocation fails, original vector unchanged
- If element copy throws, new storage freed, original unchanged
Growth Strategy
Vector must grow when full. Options:
- Add fixed amount: O(n) amortized push_back
- Double capacity: O(1) amortized push_back
Why doubling works:
push_back operations: 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
reallocations at: 1, 2, 4, 8 (capacity doubles)
cost at realloc: 1, 2, 4, 8 (copy old elements)
total cost for n: O(n) copies for n elements = O(1) amortized
2.2 Why This Matters
Understanding container implementation:
- Helps choose the right container for performance
- Enables custom containers for specific needs
- Prepares you for performance-critical code
- Is frequently asked in interviews
Real-world applications:
- Game engines use custom vector with arena allocation
- Databases use custom containers for buffer management
- High-frequency trading needs cache-friendly containers
2.3 Historical Context
The STL was designed by Alexander Stepanov in the 1990s, influenced by:
- Generic programming ideas from Ada
- Mathematical concepts of iterators and algorithms
- Performance requirements for real systems
The key insight: separate containers, algorithms, and iterators:
- Containers: how data is stored
- Algorithms: operations on data
- Iterators: abstraction connecting them
This means N containers + M algorithms = N*M combinations with only N+M implementations.
2.4 Common Misconceptions
Misconception 1: “vector::push_back is always O(1)” False. Individual push_back can be O(n) when reallocating. But amortized over many push_backs, it’s O(1).
Misconception 2: “list is always better for insertions” False. Vector is usually faster due to cache locality, even for middle insertions, unless you’re inserting many elements in the middle of a very large container.
Misconception 3: “Iterators are like pointers” Partially true. Vector iterators often are pointers. But list iterators are objects wrapping node pointers. Iterator invalidation rules differ.
Misconception 4: “unordered_map is always faster than map” False. For small sizes, map (tree-based) can be faster. Hash computation and cache effects matter.
3. Project Specification
3.1 What You Will Build
A mini-STL with:
MyVector<T>- Dynamic array with amortized O(1) push_backMyList<T>- Doubly-linked list with O(1) insert/eraseMyUnorderedMap<K, V>- Hash table with O(1) average lookup- Iterators that work with standard algorithms (
std::sort,std::find)
3.2 Functional Requirements
| ID | Requirement |
|---|---|
| F1 | MyVector: dynamic growth with capacity doubling |
| F2 | MyVector: push_back, pop_back, insert, erase |
| F3 | MyVector: random access iterator compatible with std::sort |
| F4 | MyVector: at() with bounds checking |
| F5 | MyList: doubly-linked with O(1) insert/erase at iterator |
| F6 | MyList: push_front, push_back, splice |
| F7 | MyList: bidirectional iterator |
| F8 | MyUnorderedMap: hash table with separate chaining |
| F9 | MyUnorderedMap: operator[], at(), find(), insert(), erase() |
| F10 | MyUnorderedMap: rehash when load factor exceeds threshold |
3.3 Non-Functional Requirements
| ID | Requirement |
|---|---|
| N1 | Vector: O(1) amortized push_back |
| N2 | Vector: strong exception guarantee for push_back |
| N3 | List: O(1) insert and erase |
| N4 | HashMap: O(1) average lookup, insert, erase |
| N5 | All containers: no memory leaks |
| N6 | All iterators: work with standard algorithms |
3.4 Example Usage / Output
// MyVector works like std::vector
MyVector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " "; // 1 2 3
}
// Works with standard algorithms!
std::sort(v.begin(), v.end(), std::greater<int>());
// v is now: 3 2 1
v.insert(v.begin() + 1, 10); // v: 3 10 2 1
v.erase(v.begin()); // v: 10 2 1
// MyList is a doubly-linked list
MyList<std::string> names;
names.push_back("Alice");
names.push_front("Bob");
auto it = std::find(names.begin(), names.end(), "Alice");
names.insert(it, "Charlie"); // Insert before Alice
// names: Bob Charlie Alice
// MyUnorderedMap is a hash map
MyUnorderedMap<std::string, int> ages;
ages["Alice"] = 30;
ages["Bob"] = 25;
ages.insert({"Charlie", 35});
std::cout << ages.at("Alice"); // 30
std::cout << ages["Unknown"]; // 0 (default-inserted)
if (ages.find("Bob") != ages.end()) {
std::cout << "Found Bob!\n";
}
ages.erase("Charlie");
std::cout << ages.size(); // 2
3.5 Real World Outcome
After completing this project, you will:
- Understand how STL containers work internally
- Know when to use which container based on performance
- Be able to implement custom containers for specific needs
- Have deep knowledge for systems programming interviews
4. Solution Architecture
4.1 High-Level Design
MyVector
+------------------+
| MyVector |
|------------------| +-----+-----+-----+-----+-----+
| T* data_ |---->| 0 | 1 | 2 | | |
| size_t size_ | +-----+-----+-----+-----+-----+
| size_t capacity_ | [0] [1] [2] [unused space]
| Allocator alloc_ |
+------------------+
size_ = 3, capacity_ = 5
MyList
+------------------+
| MyList |
|------------------|
| Node* head_ |
| Node* tail_ |
| size_t size_ |
+------------------+
|
v
+------+ +------+ +------+
| prev |<-->| prev |<-->| prev |
| data | | data | | data |
| next | | next | | next |
+------+ +------+ +------+
head tail
MyUnorderedMap
+---------------------+
| MyUnorderedMap |
|---------------------| +-------+-------+-------+-------+
| Bucket* buckets_ |---->| 0 | 1 | 2 | 3 |
| size_t bucket_count | +---+---+---+---+---+---+---+---+
| size_t size_ | | |
| float max_load_ | v v
+---------------------+ +-----+ +-----+
|K1,V1| |K2,V2|
+--+--+ +--+--+
| |
v v
+-----+ nullptr
|K3,V3|
+-----+
|
v
nullptr
4.2 Key Components
MyVector
template<typename T, typename Allocator = std::allocator<T>>
class MyVector {
T* data_ = nullptr;
size_t size_ = 0;
size_t capacity_ = 0;
Allocator alloc_;
public:
using iterator = T*;
using const_iterator = const T*;
iterator begin() { return data_; }
iterator end() { return data_ + size_; }
void push_back(const T& value);
void push_back(T&& value);
void pop_back();
iterator insert(iterator pos, const T& value);
iterator erase(iterator pos);
void reserve(size_t new_cap);
T& operator[](size_t i) { return data_[i]; }
T& at(size_t i);
size_t size() const { return size_; }
size_t capacity() const { return capacity_; }
bool empty() const { return size_ == 0; }
};
MyList
template<typename T>
class MyList {
struct Node {
T data;
Node* prev = nullptr;
Node* next = nullptr;
};
Node* head_ = nullptr;
Node* tail_ = nullptr;
size_t size_ = 0;
public:
class iterator {
Node* node_;
public:
T& operator*() { return node_->data; }
iterator& operator++() { node_ = node_->next; return *this; }
iterator& operator--() { node_ = node_->prev; return *this; }
bool operator==(const iterator& other) { return node_ == other.node_; }
};
iterator begin() { return iterator(head_); }
iterator end() { return iterator(nullptr); }
void push_front(const T& value);
void push_back(const T& value);
iterator insert(iterator pos, const T& value);
iterator erase(iterator pos);
};
MyUnorderedMap
template<typename K, typename V, typename Hash = std::hash<K>>
class MyUnorderedMap {
struct Node {
std::pair<const K, V> kv;
Node* next = nullptr;
};
Node** buckets_ = nullptr;
size_t bucket_count_ = 0;
size_t size_ = 0;
float max_load_factor_ = 1.0f;
Hash hash_;
public:
V& operator[](const K& key);
V& at(const K& key);
iterator find(const K& key);
std::pair<iterator, bool> insert(const std::pair<K, V>& kv);
size_t erase(const K& key);
void rehash(size_t new_bucket_count);
};
4.3 Data Structures
| Container | Internal Structure | Iterator Type |
|---|---|---|
| MyVector | Contiguous array | Raw pointer (random access) |
| MyList | Doubly-linked nodes | Node pointer wrapper (bidirectional) |
| MyUnorderedMap | Array of buckets + linked nodes | Node pointer wrapper (forward) |
4.4 Algorithm Overview
Vector Growth (push_back)
if size == capacity:
new_cap = capacity == 0 ? 1 : capacity * 2
new_data = allocate(new_cap)
for i in 0 to size:
construct(new_data + i, move_if_noexcept(data[i]))
destroy all in old data
deallocate old data
data = new_data
capacity = new_cap
construct(data + size, value)
size++
List Insert
new_node = new Node(value)
new_node->prev = pos.node->prev
new_node->next = pos.node
if pos.node->prev:
pos.node->prev->next = new_node
else:
head = new_node
pos.node->prev = new_node
size++
return iterator(new_node)
HashMap Insert
if size / bucket_count > max_load_factor:
rehash(bucket_count * 2)
bucket_idx = hash(key) % bucket_count
for node in buckets[bucket_idx]:
if node->key == key:
return {iterator(node), false} // Already exists
new_node = new Node{key, value}
new_node->next = buckets[bucket_idx]
buckets[bucket_idx] = new_node
size++
return {iterator(new_node), true}
5. Implementation Guide
5.1 Development Environment Setup
mkdir containers && cd containers
touch my_vector.hpp my_list.hpp my_unordered_map.hpp
touch test_containers.cpp
# Compile with all warnings
g++ -std=c++17 -Wall -Wextra -Wpedantic -o test test_containers.cpp
# With sanitizers
g++ -std=c++17 -Wall -Wextra -fsanitize=address,undefined -o test test_containers.cpp
5.2 Project Structure
containers/
├── include/
│ ├── my_vector.hpp
│ ├── my_list.hpp
│ └── my_unordered_map.hpp
├── tests/
│ ├── test_vector.cpp
│ ├── test_list.cpp
│ └── test_unordered_map.cpp
└── benchmarks/
└── benchmark.cpp
5.3 The Core Question You’re Answering
“How do I create generic, efficient, and safe data structures that work with any type and any algorithm?”
This decomposes into:
- How do I write type-generic code (templates)?
- How do I manage memory for dynamic containers (allocators)?
- How do I provide a uniform interface for traversal (iterators)?
- How do I maintain safety when operations fail (exception safety)?
5.4 Concepts You Must Understand First
| Question | Book Reference |
|---|---|
| How do template classes work? | C++ Programming Language Ch. 23 |
| What are iterator categories? | Effective STL Items 1-4 |
| How does std::allocator work? | C++ Programming Language Ch. 34 |
| What is the strong exception guarantee? | Exceptional C++ Items 8-17 |
| How does placement new work? | C++ Programming Language Ch. 11 |
5.5 Questions to Guide Your Design
Vector Design
- What growth factor should you use? (1.5x, 2x, or other?)
- How do you handle types that throw on copy?
- What should operator[] return for out-of-bounds?
- How do you implement insert in the middle efficiently?
List Design
- How do you handle end() iterator (past the last element)?
- Should you use a sentinel node?
- How do you implement splice in O(1)?
- What happens when you erase the only element?
HashMap Design
- What hash function to use for strings?
- How do you handle hash collisions?
- When do you rehash?
- What if the user provides a bad hash function?
Iterator Design
- What traits must your iterator provide?
- How do you handle const_iterator vs iterator?
- What happens when container is modified during iteration?
- How do you make iterators work with standard algorithms?
5.6 Thinking Exercise
Trace through this code:
MyVector<std::string> v;
v.reserve(2); // capacity = 2, size = 0
v.push_back("hello"); // size = 1
v.push_back("world"); // size = 2
v.push_back("!"); // reallocation happens
// What is capacity after the third push_back?
// What if std::string copy throws during reallocation?
Trace:
reserve(2): allocate space for 2 strings, size=0, capacity=2push_back("hello"): construct at data[0], size=1push_back("world"): construct at data[1], size=2, capacity=2 (full)push_back("!"):- Allocate new buffer of capacity 4 (doubled)
- Move “hello” and “world” to new buffer (using move_if_noexcept)
- Destroy old strings and deallocate old buffer
- Construct “!” at position 2
- size=3, capacity=4
If string copy throws during step 4:
- Destroy any strings already copied to new buffer
- Deallocate new buffer
- Original vector unchanged (strong guarantee)
5.7 Hints in Layers
Hint 1: Starting Point (Conceptual) Start with a minimal vector: just push_back, size, and operator[]. No iterators, no allocators yet. Get that working first.
Hint 2: Next Level (Vector skeleton)
template<typename T>
class MyVector {
T* data_ = nullptr;
size_t size_ = 0;
size_t capacity_ = 0;
public:
~MyVector() {
for (size_t i = 0; i < size_; ++i)
data_[i].~T();
::operator delete(data_);
}
void push_back(const T& value) {
if (size_ == capacity_) {
size_t new_cap = capacity_ == 0 ? 1 : capacity_ * 2;
T* new_data = static_cast<T*>(::operator new(new_cap * sizeof(T)));
for (size_t i = 0; i < size_; ++i)
new (new_data + i) T(std::move(data_[i]));
for (size_t i = 0; i < size_; ++i)
data_[i].~T();
::operator delete(data_);
data_ = new_data;
capacity_ = new_cap;
}
new (data_ + size_) T(value);
++size_;
}
T& operator[](size_t i) { return data_[i]; }
size_t size() const { return size_; }
};
Hint 3: Adding Allocator Support
template<typename T, typename Allocator = std::allocator<T>>
class MyVector {
using AllocTraits = std::allocator_traits<Allocator>;
T* data_ = nullptr;
size_t size_ = 0;
size_t capacity_ = 0;
Allocator alloc_;
T* allocate(size_t n) {
return AllocTraits::allocate(alloc_, n);
}
void deallocate(T* p, size_t n) {
AllocTraits::deallocate(alloc_, p, n);
}
template<typename... Args>
void construct(T* p, Args&&... args) {
AllocTraits::construct(alloc_, p, std::forward<Args>(args)...);
}
void destroy(T* p) {
AllocTraits::destroy(alloc_, p);
}
};
Hint 4: Exception-Safe reserve
void reserve(size_t new_cap) {
if (new_cap <= capacity_) return;
T* new_data = allocate(new_cap);
size_t constructed = 0;
try {
for (; constructed < size_; ++constructed) {
construct(new_data + constructed,
std::move_if_noexcept(data_[constructed]));
}
} catch (...) {
for (size_t i = 0; i < constructed; ++i) {
destroy(new_data + i);
}
deallocate(new_data, new_cap);
throw;
}
for (size_t i = 0; i < size_; ++i) {
destroy(data_ + i);
}
if (data_) {
deallocate(data_, capacity_);
}
data_ = new_data;
capacity_ = new_cap;
}
5.8 The Interview Questions They’ll Ask
- “What is the amortized complexity of vector::push_back?”
- O(1) amortized
- Individual push_back can be O(n) when reallocating
- But doubling strategy means total cost is O(n) for n pushes
- “Why does vector use contiguous memory?”
- Cache locality: prefetching works well
- Random access: O(1) by pointer arithmetic
- Interoperability: can pass data_ to C APIs
- “When would you use list over vector?”
- Frequent insertions/deletions in the middle (with iterator)
- Never invalidating iterators is required
- Objects are very large (no copying on reallocation)
- “How does unordered_map handle collisions?”
- Separate chaining: each bucket is a linked list
- Or open addressing: probe for next empty slot
- Standard library uses separate chaining
- “What is iterator invalidation?”
- Some operations invalidate existing iterators
- Vector: push_back may invalidate all; insert/erase invalidates at/after position
- List: only erase invalidates the erased iterator
- Map: only erase invalidates the erased iterator
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Templates | The C++ Programming Language | Chapters 23-25 |
| STL Design | Effective STL | All |
| Iterator Concepts | C++ Standard Library | Chapter 7 |
| Allocators | The C++ Programming Language | Chapter 34 |
| Exception Safety | Exceptional C++ | Items 8-17 |
5.10 Implementation Phases
Phase 1: Basic Vector (Day 1-4)
- Constructor, destructor
- push_back (with growth)
- operator[], at()
- size(), capacity(), empty()
- Test: basic operations work
Phase 2: Vector Iterators (Day 5-6)
- typedef T* as iterator
- begin(), end()
- Test: std::sort and std::find work
Phase 3: Vector Full API (Day 7-9)
- insert, erase
- reserve, resize
- Move semantics (push_back with T&&)
- Copy/move constructors and assignment
- Test: exception safety
Phase 4: List (Day 10-14)
- Node struct, head/tail pointers
- push_front, push_back
- Bidirectional iterator
- insert, erase
- Test: all operations, splice
Phase 5: Unordered Map (Day 15-21)
- Bucket array with linked lists
- operator[], at(), find()
- insert, erase
- rehash when load factor exceeded
- Test: correctness and O(1) average
Phase 6: Polish (Day 22-24)
- Add const_iterator to all
- Ensure no leaks (Valgrind)
- Compare performance with std::
5.11 Key Implementation Decisions
| Decision | Options | Recommendation |
|---|---|---|
| Vector growth | 1.5x vs 2x | 2x (simpler, standard) |
| List sentinel | With vs without | Without (simpler for learning) |
| HashMap collision | Chaining vs open addressing | Chaining (easier) |
| HashMap bucket count | Prime vs power of 2 | Power of 2 (faster modulo) |
| Iterator validity | Strict checks vs trust | Trust (like STL) |
6. Testing Strategy
6.1 Unit Tests
// Vector tests
void testVectorGrowth() {
MyVector<int> v;
for (int i = 0; i < 1000; ++i) {
v.push_back(i);
}
assert(v.size() == 1000);
assert(v.capacity() >= 1000);
for (int i = 0; i < 1000; ++i) {
assert(v[i] == i);
}
}
void testVectorWithStdSort() {
MyVector<int> v;
v.push_back(3);
v.push_back(1);
v.push_back(2);
std::sort(v.begin(), v.end());
assert(v[0] == 1 && v[1] == 2 && v[2] == 3);
}
// List tests
void testListInsert() {
MyList<int> list;
list.push_back(1);
list.push_back(3);
auto it = list.begin();
++it;
list.insert(it, 2); // Insert 2 before 3
// List should be: 1, 2, 3
it = list.begin();
assert(*it++ == 1);
assert(*it++ == 2);
assert(*it++ == 3);
}
// HashMap tests
void testHashMapBasic() {
MyUnorderedMap<std::string, int> map;
map["one"] = 1;
map["two"] = 2;
assert(map.size() == 2);
assert(map["one"] == 1);
assert(map.at("two") == 2);
map.erase("one");
assert(map.find("one") == map.end());
}
6.2 Edge Cases to Test
| Container | Edge Case | Expected |
|---|---|---|
| Vector | push_back to empty | Works, capacity >= 1 |
| Vector | erase last element | size decreases, valid |
| Vector | insert at begin | All elements shifted |
| List | erase only element | Empty list, valid |
| List | push_front to empty | size 1, head == tail |
| HashMap | many collisions | Still O(n) worst case |
| HashMap | rehash during iteration | Implementation-defined |
| All | const container | Only const methods work |
7. Common Pitfalls & Debugging
| Problem | Symptom | Root Cause | Fix |
|---|---|---|---|
| Double free | Crash | Shallow copy | Implement copy constructor properly |
| Iterator invalidation | Crash or wrong data | Using iterator after push_back | Re-obtain iterator after modifications |
| Memory leak | Growing memory | Not destroying elements | Call destructor before deallocate |
| Undefined behavior | Anything | Out of bounds access | Check bounds in at() |
| Hash collision loop | Infinite loop | Bad equality check | Ensure operator== is correct |
Debugging Tips
- Add bounds checking: In debug mode, check all accesses
- Print on construct/destruct: Track object lifetimes
- Use Valgrind: Catch leaks and invalid access
- Test with throwing types: Verify exception safety
- Compare with std: Same operations should give same results
8. Extensions & Challenges
| Extension | Difficulty | Concepts Learned |
|---|---|---|
| Small buffer optimization (vector) | Medium | In-place storage for small containers |
| Intrusive list | Medium | Embedding links in elements |
| Robin Hood hashing | Hard | Open addressing variant |
| Custom allocator | Medium | Pool allocation |
| Concurrent containers | Hard | Lock-free or lock-based thread safety |
9. Real-World Connections
How the Standard Library Implements These
| Container | libstdc++ | libc++ |
|---|---|---|
| vector | Dynamic array, growth ~2x | Similar |
| list | Doubly-linked with sentinel | Similar |
| unordered_map | Separate chaining | Separate chaining |
When to Use Each
| Use Case | Container | Why |
|---|---|---|
| Unknown size, random access | vector | Cache friendly, fast |
| Frequent middle insertion | list | O(1) insert |
| Key-value lookup | unordered_map | O(1) average |
| Ordered traversal | map (tree) | Sorted iteration |
| Priority queue | priority_queue (heap) | O(log n) push/pop |
10. Resources
Primary References
- Stroustrup, B. “The C++ Programming Language” - Chapters 23-25, 34
- Meyers, S. “Effective STL” - All items
- cppreference: Containers
Online Resources
11. Self-Assessment Checklist
Before considering this project complete, verify:
- Vector grows correctly (capacity doubles)
- Vector iterators work with std::sort
- Vector provides strong exception guarantee for push_back
- List insert/erase are O(1)
- List iterators are bidirectional (work with std::reverse)
- HashMap has O(1) average lookup
- HashMap rehashes when load factor exceeded
- All containers: no memory leaks
- All containers: const versions of methods work
- Performance comparable to std:: versions
12. Submission / Completion Criteria
This project is complete when:
- All functional requirements (F1-F10) are implemented
- Example code from section 3.4 works correctly
- Iterators work with standard algorithms (sort, find, reverse)
- Exception safety maintained (strong for push_back)
- Performance within 2x of standard library
- No memory leaks (Valgrind clean)
- All unit tests pass
- You can explain the amortized complexity of vector::push_back
Deliverables:
- Header files for each container
- Comprehensive test suite
- Benchmark comparing to std:: versions
- Brief writeup explaining design decisions
This project teaches you how the tools you use every day actually work. Understanding container internals helps you choose the right container, debug performance issues, and write your own specialized containers when needed. The patterns here—templates, iterators, allocators—are the foundation of modern C++ library design.