Implement Your Own std::vector<T> (MyVector)
In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.
std::vector guarantees the Strong Exception Safety Guarantee: if an operation fails, the vector's state remains exactly as it was before the call. During reallocation, if elements were moved from the old buffer to the new buffer and element k throws an exception, the already-moved elements in the old buffer are permanently altered or corrupted. Therefore, if std::is_nothrow_move_constructible_v<T> is false, std::vector refuses to move and falls back to copying each element so that the original buffer remains undamaged if an exception occurs.
reserve(n): Alters only capacity(). If $n > capacity, it allocates raw uninitialized storage for at least n$ elements. It does not construct any objects, does not change size(), and operator[] cannot be used beyond the existing size().
resize(n): Alterssize(). If $n > size, it default-constructs (or value-constructs) n - size new instances. If n < size$, it invokes destructors on the excess elements, reducingsize()while keepingcapacity()unchanged.
A growth factor of 2.0Γ ensures clean amortized O(1) appends, but geometrically, the sum of all previously deallocated memory blocks $\sum_{i=0}^{k-1} 2^i = 2^k - 1 < 2^k is strictly less than the next required block 2^k. Thus, an allocator can never reuse previously freed buffer memory for the next reallocation. In MSVC, a growth factor of 1.5Γ is used because \sum_{i=0}^{k-1} 1.5^i eventually exceeds 1.5^k$, enabling the heap allocator to recycle previously freed memory chunks, reducing virtual memory fragmentation.
Interviewer's lens
Asking candidates to implement std::vector<T> from scratch is the single most revealing systems C++ interview question because it immediately separates beginners who treat memory as high-level syntax from systems engineers who understand the separation of raw byte allocation from object lifetime. Weak candidates declare new T[cap], which fails to compile for types without a default constructor and unnecessarily constructs uninitialized objects. Senior candidates immediately write ::operator new for raw uninitialized byte storage, placement new (std::construct_at) for on-demand construction, and explicit std::destroy_at for destruction, while proactively solving the dangerous v.push_back(v[0]) self-referencing reallocation bug and enforcing noexcept move transfers.
Under the hood & memory layout
At the machine level, std::vector<T> is a compact 24-byte control struct consisting of three raw 64-bit pointers (or a pointer and two size_t counters): begin_, end_, and end_of_storage_ (or data_, size_, and capacity_). Crucially, the heap buffer between data_ + size_ and data_ + capacity_ contains raw, uninitialized bytes. The class cannot invoke new T[cap] because array new mandates default construction of all cap objects. Instead, memory is requested from the global allocator via ::operator new(cap * sizeof(T)). When an element is added, placement new or std::construct_at executes T's constructor directly inside the pre-allocated byte slot. When pop_back() is invoked, T's destructor is called explicitly without returning memory to the OS.
Stores T* data_ (8 bytes), size_t size_ (8 bytes), and size_t capacity_ (8 bytes). sizeof(MyVector<T>) == 24 on 64-bit systems regardless of T.
Contains size_ live, fully constructed T instances. Calling destructors or deallocating the vector requires destroying each element in this range via std::destroy_at.
Contains (capacity_ - size_) * sizeof(T) bytes of allocated, uninitialized memory. Reading or invoking member functions on this region is immediate Undefined Behavior.
When size_ == capacity_, a new buffer of capacity_ * 2 * sizeof(T) bytes is allocated. Elements are relocated via std::move_if_noexcept, the old buffer elements are destroyed, and ::operator delete(data_) reclaims the old slab.
Core concepts & syntax traps
Why new T[cap] Is Banned in Production std::vector: Placement new & std::construct_at
Why new T[cap] Is Banned in Production std::vector: Placement new & std::construct_at
A classic junior mistake when implementing vector is writing:
// FATAL JUNIOR MISTAKE:
data_ = new T[capacity_];
This fails catastrophically for three reasons:
- Mandates Default Constructor: If
Thas no default constructor (e.g.struct User { User(int id); };),new T[cap]will not compile! - Performance Waste: Writing
reserve(1'000'000)instantly invokes 1,000,000 default constructors, only for subsequentpush_backcalls to overwrite those values. - Double Destruction:
delete[]expects allcapacity_objects to be valid. If only 3 elements were pushed, deleting will destroy uninitialized or overwritten objects.
A true vector allocates raw uninitialized memory with ::operator new(bytes), constructs objects in place using placement new / std::construct_at, and manually calls std::destroy_at on live elements.
// 1. Raw memory allocation (0 constructors called!)
T* buf = static_cast<T*>(::operator new(newCap * sizeof(T)));
// 2. In-place construction at address (newBuf + size_)
std::construct_at(buf + size_, std::forward<Args>(args)...);
// 3. Explicit destruction without releasing raw memory
std::destroy_at(buf + idx);
// 4. Raw deallocation
::operator delete(buf);Never use array new for custom containers; separate raw allocation (::operator new) from object construction (placement new).
Geometric 2x Growth Factor: Deriving Amortized O(1) Push Back
Geometric 2x Growth Factor: Deriving Amortized O(1) Push Back
Why does std::vector double its capacity ($1 β 2 β 4 β 8 β¦) rather than growing by a fixed constant like +10 or +100$?
If capacity grows by a constant C, inserting N elements triggers N / C reallocations. Each reallocation copies the existing i cdot C elements, resulting in:
$$\sum_{k=1}^{N/C} k \cdot C = O(N^2) total copies \implies O(N) per push_back!$$
With geometric doubling (2Γ), inserting N elements requires at most log_2 N reallocations. The total number of element copies across all reallocations is a convergent geometric series:
$$1 + 2 + 4 + 8 + β¦ + N = 2N - 1 < 2N total copies$$
Dividing 2N copies by N operations yields an average of $< 2$ copies per push_back, proving amortized O(1) time complexity.
std::size_t newCap = capacity_ == 0 ? 1 : capacity_ * 2;
// Growth factor of 1.5x (used in MSVC) or 2x (used in GCC/Clang)
// 1.5x allows recycling previously deallocated memory chunks!Geometric scaling is mathematically required for amortized O(1) append; arithmetic growth degrades to catastrophic O(N^2) complexity.
The Self-Aliasing Trap: Why v.push_back(v[0]) Crashes Naive Vectors
The Self-Aliasing Trap: Why v.push_back(v[0]) Crashes Naive Vectors
Consider the innocent-looking call:
v.push_back(v[0]);
Assume v.size() == v.capacity() == 1. When push_back(const T& val) is called, parameter val is a const reference binding directly to v.data_[0]. A naive implementation writes:
void push_back(const T& val) {
if (size_ == capacity_) {
reserve(capacity_ * 2); // REALLOCATES AND FREES OLD BUFFER!
}
std::construct_at(data_ + size_, val); // val is DANGLING! Reads freed memory!
}
The moment reserve() deallocates the old buffer, val becomes a dangling reference to dead heap memory! To prevent this, the vector must construct the new element in the newly allocated buffer before deallocating the old buffer.
// Safe push_back during reallocation:
T* newBuf = static_cast<T*>(::operator new(newCap * sizeof(T)));
// 1. Construct target from val FIRST while old buffer is still alive
std::construct_at(newBuf + size_, val);
// 2. Relocate existing elements from old buffer
for (std::size_t i = 0; i < size_; ++i) {
std::construct_at(newBuf + i, std::move_if_noexcept(data_[i]));
std::destroy_at(data_ + i);
}
::operator delete(data_);
data_ = newBuf;Always construct the new element in the new memory block before destroying or freeing the old buffer.
emplace_back vs push_back: Universal References & Perfect Forwarding
emplace_back vs push_back: Universal References & Perfect Forwarding
push_back accepts either const T& (copy) or T&& (move), meaning an existing object must already exist before being placed into the container. For example, v.push_back(User(1, "Alice")) creates a temporary User on the stack and moves it into the vector buffer.
emplace_back(Args&&... args) uses a variadic template parameter pack and forward references (T&&). Instead of constructing a temporary object, it forwards the constructor arguments directly into placement new:
std::construct_at(data_ + size_, std::forward<Args>(args)...);
The object is constructed directly inside the vector's heap buffer with zero temporary objects and zero copy/move operations.
template <typename... Args>
T& emplace_back(Args&&... args) {
if (size_ == capacity_) {
// reallocate safely...
}
T* p = std::construct_at(data_ + size_, std::forward<Args>(args)...);
++size_;
return *p; // C++17 returns reference to constructed element
}emplace_back constructs directly in-place via std::forward, eliminating temporary construction and move overhead.
Strong Exception Safety & Move-If-Noexcept Reallocation
Strong Exception Safety & Move-If-Noexcept Reallocation
When reallocating a vector from capacity_ to 2 * capacity_, what happens if copying element k throws an exception (e.g. std::bad_alloc)?
If we moved elements using std::move and an exception is thrown midway, the original vector is corrupted: some elements are moved-from zombies, while others remain in the old buffer! To satisfy the Strong Exception Safety Guarantee (commit-or-rollback):
- Use
std::move_if_noexcept: IfT's move constructor is markednoexcept, move the element; otherwise, make a copy. - Wrap the construction loop in a
try/catchblock. If an exception throws, destroy the newly constructed elements, deallocate the new buffer, and rethrow, leaving the original vector completely untouched.
std::size_t constructed = 0;
try {
for (std::size_t i = 0; i < size_; ++i) {
std::construct_at(newBuf + i, std::move_if_noexcept(data_[i]));
++constructed;
}
} catch (...) {
for (std::size_t i = 0; i < constructed; ++i) std::destroy_at(newBuf + i);
::operator delete(newBuf);
throw; // Strong guarantee: original data_ is still 100% intact!
}Mark custom class move constructors noexcept; otherwise std::vector falls back to slow copies during reallocation to preserve strong exception safety.
Unlock Live Implementation & Interview Scenarios
The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.