2Brute2Best
πŸ”₯0
M6.2

Implement Your Own std::vector<T> (MyVector)

Hard High-frequency interview topic Pro OnlyRaw Buffer AllocationPlacement new & ~T()2x Geometric GrowthRule of 5push_back / emplace_back
⚑ 15-Second Mental ModelM6.2 · Instant Intuition
Single-Read Mastery
The Real-World Physical Analogy (Core Intuition)

In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.

Rule to Remember: Make illegal states impossible to compile or run.
3 Golden Facts to Remember
Fact #1
Why does std::vector require move constructors to be marked noexcept to use move operations during reallocation?

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.

Fact #2
What is the difference between reserve() and resize() in std::vector?

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): Alters size(). If $n > size, it default-constructs (or value-constructs) n - size new instances. If n < size$, it invokes destructors on the excess elements, reducing size() while keeping capacity() unchanged.
Fact #3
Why is the growth factor of std::vector typically 1.5x in MSVC and 2.0x in GCC/Clang?

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.

01

Interviewer's lens

What separates a strong C++ candidate from a textbook answer

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.

02

Under the hood & memory layout

Hardware & memory representation

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.

Vector Control Block (24 bytes)

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.

Constructed Region [0 .. size_-1]

Contains size_ live, fully constructed T instances. Calling destructors or deallocating the vector requires destroying each element in this range via std::destroy_at.

Uninitialized Tail [size_ .. capacity_-1]

Contains (capacity_ - size_) * sizeof(T) bytes of allocated, uninitialized memory. Reading or invoking member functions on this region is immediate Undefined Behavior.

Reallocation Buffer Swap

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.

03

Core concepts & syntax traps

01

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:

  1. Mandates Default Constructor: If T has no default constructor (e.g. struct User { User(int id); };), new T[cap] will not compile!
  2. Performance Waste: Writing reserve(1'000'000) instantly invokes 1,000,000 default constructors, only for subsequent push_back calls to overwrite those values.
  3. Double Destruction: delete[] expects all capacity_ 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.

C++23 snippet
// 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);
Interview takeaway:

Never use array new for custom containers; separate raw allocation (::operator new) from object construction (placement new).

02

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.

C++23 snippet
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!
Interview takeaway:

Geometric scaling is mathematically required for amortized O(1) append; arithmetic growth degrades to catastrophic O(N^2) complexity.

03

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.

C++23 snippet
// 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;
Interview takeaway:

Always construct the new element in the new memory block before destroying or freeing the old buffer.

04

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.

C++23 snippet
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
}
Interview takeaway:

emplace_back constructs directly in-place via std::forward, eliminating temporary construction and move overhead.

05

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):

  1. Use std::move_if_noexcept: If T's move constructor is marked noexcept, move the element; otherwise, make a copy.
  2. Wrap the construction loop in a try/catch block. If an exception throws, destroy the newly constructed elements, deallocate the new buffer, and rethrow, leaving the original vector completely untouched.
C++23 snippet
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!
}
Interview takeaway:

Mark custom class move constructors noexcept; otherwise std::vector falls back to slow copies during reallocation to preserve strong exception safety.

Brute2Best Pro Content

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.

All 440 visual dry runs & algorithms
Interactive step-by-step playback with pointer & array state transitions
52 comprehensive LLD chapters
Interactive UML diagrams, real-world stories (Food Delivery, Trading, RPG), and practice solutions
40 C++ basics labs & hardware memory models
Vtables, byte layouts, assembly insights, and multi-threading race conditions
Production-ready C++23 source code
Modern idioms, clean contracts, time/space complexity derivations, and interview takeaways

First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.

Next In Your Mastery Journey

Apply C++ Internals to LLD & Algorithmic Patterns

Full-stack systems engineering