Project 1: The Exception-Safe Vector
Build a
Vector<T>that never corrupts state, even when allocations or element construction fail.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Advanced |
| Time Estimate | 1-2 weeks |
| Main Programming Language | C++17 (Alternatives: Rust for comparison) |
| Coolness Level | Genuinely Clever |
| Business Potential | Resume Gold |
| Prerequisites | C++ fundamentals, RAII, templates, basic allocator knowledge |
| Key Topics | Exception guarantees, RAII, copy-and-swap, noexcept |
1. Learning Objectives
By completing this project, you will:
- Provide strong exception safety for container mutation.
- Use RAII to guarantee cleanup on failure.
- Apply copy-and-swap to implement safe assignment.
- Reason about
noexceptand move vs copy trade-offs. - Build a failure-injection test harness for
std::bad_alloc.
2. All Theory Needed (Per-Concept Breakdown)
2.1 Exception Guarantees
Description
C++ code is correct only if it remains correct under failure. The three guarantees define what remains true after an exception: basic (valid but changed), strong (unchanged), and no-throw (never throws).
Definitions & Key Terms
- Basic guarantee: invariants preserved, no leaks.
- Strong guarantee: commit-or-rollback semantics.
- No-throw guarantee: operation never throws.
Mental Model Diagram (ASCII)
Operation
| success -> new state
| failure -> rollback (strong) OR valid-but-changed (basic)
How It Works (Step-by-Step)
- Identify every operation that can throw.
- Perform risky work on temporary state.
- Commit only after all risk passes.
Minimal Concrete Example
T& operator=(T other) noexcept { swap(other); return *this; }
Common Misconceptions
- “Strong guarantee is always required” -> not always achievable or needed.
Check-Your-Understanding Questions
- Which operations inside
push_backcan throw? - Why is
swapcentral to strong guarantees?
Where You’ll Apply It
- Project 1 (this file), also referenced in P02 (lock guards)
2.2 RAII and Ownership
Description
RAII ties resource lifetime to object lifetime. It is the foundation of exception-safe code because destructors always run during stack unwinding.
Definitions & Key Terms
- RAII: resource acquisition is initialization.
- Ownership: single clear owner responsible for cleanup.
Mental Model Diagram (ASCII)
[Resource] --owned by--> [Object] --destroyed--> [Resource released]
How It Works (Step-by-Step)
- Acquire resource in constructor.
- Store resource handle in object.
- Release in destructor.
Minimal Concrete Example
std::unique_ptr<T[]> buf(new T[n]);
Common Misconceptions
- “RAII is just smart pointers” -> it applies to any resource.
Check-Your-Understanding Questions
- Why do destructors run during exception unwinding?
- What is the difference between owning and non-owning pointers?
Where You’ll Apply It
- Project 1 resize logic and temporary buffers
2.3 Copy-and-Swap
Description
Copy-and-swap builds a safe new state, then atomically swaps it with the old state. It makes assignment strongly exception-safe if swap is noexcept.
Definitions & Key Terms
- Swap: exchange two object states.
- Commit step: point where new state replaces old.
Mental Model Diagram (ASCII)
old_state ----copy----> temp
old_state <---swap---- temp
(temp destroys old)
How It Works (Step-by-Step)
- Create a copy of the right-hand side.
- Swap with
*this. - Old state destructs when temp goes out of scope.
Minimal Concrete Example
void swap(Vector& other) noexcept { std::swap(data_, other.data_); }
Common Misconceptions
- “Swap is always cheap” -> it is if it only swaps pointers.
Check-Your-Understanding Questions
- Why is self-assignment safe with copy-and-swap?
- What happens if swap throws?
Where You’ll Apply It
- Project 1 assignment operator
2.4 noexcept and Move Semantics
Description
The standard library uses noexcept to decide whether moves are safe. If move can throw, containers may copy instead.
Definitions & Key Terms
noexcept: declared non-throwing function.- Move constructor: steals resources from a temporary.
Mental Model Diagram (ASCII)
if (move is noexcept) -> move elements
else -> copy elements
How It Works (Step-by-Step)
- The compiler sees a move constructor.
- If it is
noexcept, containers prefer it. - If not, containers may copy to preserve strong guarantees.
Minimal Concrete Example
Vector(Vector&& other) noexcept : data_(other.data_) { other.data_ = nullptr; }
Common Misconceptions
- “Mark everything noexcept” -> only if truly safe.
Check-Your-Understanding Questions
- Why do containers avoid throwing moves during reallocation?
Where You’ll Apply It
- Project 1 move constructor, swap, destructor
3. Project Specification
3.1 What You Will Build
A minimal Vector<T> supporting push_back, size, capacity, and safe copy/move operations, with strong exception guarantees for reallocation paths.
3.2 Functional Requirements
- Dynamic storage with doubling growth strategy.
push_backprovides strong exception safety.- Copy/move constructors follow value semantics.
- Safe assignment using copy-and-swap.
- Iterators or index access for validation.
3.3 Non-Functional Requirements
- Reliability: no leaks under ASan/Valgrind.
- Exception safety: strong guarantee on
push_back. - Performance: no unnecessary deep copies during reallocation.
3.4 Example Usage / Output
Vector<int> v;
for (int i = 0; i < 3; ++i) v.push_back(i);
std::cout << v.size() << "/" << v.capacity() << "\n";
3.5 Data Formats / Schemas / Protocols
- Internal layout:
T* data_,size_t size_,size_t capacity_ - Growth:
capacity_ = max(1, capacity_ * 2)
3.6 Edge Cases
push_backon empty vectorpush_backwhen allocation fails- Self-assignment
- Move from empty vector
3.7 Real World Outcome
3.7.1 How to Run (Copy/Paste)
cmake -S . -B build
cmake --build build
./build/vector_test --fail-after=5 --seed=42
3.7.2 Golden Path Demo (Deterministic)
[TEST] push_back with failure injection
[ALLOC] countdown=5
[ALLOC] countdown=4
[ALLOC] countdown=3
[ALLOC] countdown=2
[ALLOC] countdown=1
[ALLOC] countdown=0 -> throwing std::bad_alloc
[OK] caught std::bad_alloc
[OK] size=5 capacity=8
[OK] elements intact: 0 1 2 3 4
3.7.3 Failure Demo
[FAIL] size changed after exception -> BUG
4. Solution Architecture
4.1 High-Level Design
[Vector<T>]
|-- data_ (T*)
|-- size_
|-- capacity_
|-- push_back -> maybe reallocate -> swap commit
4.2 Key Components
| Component | Responsibility | Key Decisions |
|—————–|————————|——————————|
| Storage pointer | Owns contiguous memory | unique_ptr for temp safety |
| Reallocator | Grows capacity | doubling strategy |
| Assignment | strong guarantee | copy-and-swap |
4.3 Data Structures (No Full Code)
struct Vector {
T* data_;
size_t size_;
size_t capacity_;
};
4.4 Algorithm Overview
Reallocate and Push
- Allocate new buffer.
- Copy/move existing elements into new buffer.
- Commit swap.
Complexity Analysis:
- Amortized
push_back: O(1) - Reallocation: O(n)
5. Implementation Guide
5.1 Development Environment Setup
cmake -S . -B build
cmake --build build
5.2 Project Structure
vector-safe/
├── src/vector.hpp
├── src/vector.cpp
├── tests/vector_tests.cpp
└── CMakeLists.txt
5.3 The Core Question You’re Answering
“How can I guarantee that a container never corrupts its state when an allocation or copy throws?”
5.4 Concepts You Must Understand First
- RAII and stack unwinding
- Strong vs basic exception guarantees
noexceptand move semantics
5.5 Questions to Guide Your Design
- Which lines in
push_backcan throw? - When is it safe to update
size_? - How can you guarantee cleanup of partially constructed data?
5.6 Thinking Exercise
Simulate a push_back that throws on the 3rd element copy. What is the state of your old buffer and size?
5.7 The Interview Questions They’ll Ask
- What is the strong exception guarantee?
- Why should destructors be
noexcept? - How does copy-and-swap avoid leaks?
- When is
std::vectorallowed to invalidate iterators?
5.8 Hints in Layers
Hint 1: Use unique_ptr<T[]> for temporary allocations.
Hint 2: Only increment size_ after element construction succeeds.
Hint 3: Use std::swap for commit.
5.9 Books That Will Help
| Topic | Book | Chapter | |——————|——————————|————————| | Exception safety | Effective C++ | Exception safety items | | RAII | A Tour of C++ | Resource management | | Containers | The C++ Programming Language | Containers chapter |
5.10 Implementation Phases
Phase 1: Core Container (2-3 days)
- Build storage pointer, size, capacity.
- Implement
push_backwithout exceptions. - Checkpoint: basic tests pass.
Phase 2: Strong Guarantee (3-4 days)
- Implement reallocation via temp buffer and swap.
- Add failure injection.
- Checkpoint: bad_alloc does not corrupt state.
Phase 3: Polish (2-3 days)
- Add move constructor,
noexceptswap. - Add tests for self-assignment.
- Checkpoint: ASan clean.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale | |—————|—————————|—————-|—————————| | Growth factor | 1.5x / 2x | 2x | standard vector strategy | | Copy vs move | always move / conditional | conditional | preserve strong guarantee |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples | |———–|——————–|—————————| | Unit | individual methods | push_back, size, capacity | | Exception | failure handling | bad_alloc injection | | Memory | leaks/UB | ASan/Valgrind |
6.2 Critical Test Cases
- Reallocation throws and state is unchanged.
- Self-assignment leaves state valid.
- Move leaves source empty but valid.
6.3 Test Data
inputs: 0..100, fail-after=5
expected: size <= 5, elements intact
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|———————–|——————————|———————-|
| Update size too early | size changes after exception | commit after success |
| Not using RAII | leak on throw | wrap temp buffer |
| Throwing destructor | terminate on unwind | mark noexcept |
7.2 Debugging Strategies
- Use ASan to catch leaks and UAF.
- Add logs around allocation and copy.
7.3 Performance Traps
- Deep copies on every push -> fix with capacity doubling.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add
reserveandshrink_to_fit. - Add
at()with bounds checking.
8.2 Intermediate Extensions
- Add iterator support.
- Add
emplace_back.
8.3 Advanced Extensions
- Add allocator support.
- Optimize with small-buffer optimization.
9. Real-World Connections
9.1 Industry Applications
- Standard library containers
- High-performance systems needing deterministic cleanup
9.2 Related Open Source Projects
- libc++ / libstdc++ vector implementations
9.3 Interview Relevance
- Exception safety and RAII are common senior C++ interview topics.
10. Resources
10.1 Essential Reading
- Effective C++ (exception safety items)
- The C++ Programming Language (resource management)
10.2 Tools & Documentation
- ASan / UBSan (compiler flags)
10.3 Related Projects in This Series
- Next: P02 Thread-Safe Queue
11. Self-Assessment Checklist
11.1 Understanding
- I can explain strong vs basic guarantees.
- I can explain why
swapmust benoexcept.
11.2 Implementation
- All requirements met and tests pass.
- ASan shows no leaks.
11.3 Growth
- I can explain this project in an interview.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Strong guarantee on
push_back - Clean ASan run
Full Completion:
- Copy/move constructors + assignment
- Failure injection tests
Excellence:
- Custom allocator support and benchmarking