Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing a Dynamic Array Container in Modern C++

Tech 1

A custom vector implementation relies on managing three pointers that track the boundaries of allocated storage and initialized elements. This implementasion explores memory management strategies, proper deep-copy semantics, and iterator stability during reallocation.

Core Architecture

template <typename T>
class DynamicArray {
    T* _data_start;      // Beginning of allocated heap block
    T* _data_finish;     // One past the last constructed element
    T* _heap_cap;        // One past the end of allocated storage
public:
    using iterator = T*;
    using const_iterator = const T*;
    
    // Constructors, destructor, and member functions...
};

Construction Patterns

The default constructor establishes an empty container by nullifying all pointers:

DynamicArray() 
    : _data_start(nullptr)
    , _data_finish(nullptr) 
    , _heap_cap(nullptr) 
{}

For range-based initialization, traverse the input sequence and append elements individually rather than bulk copying:

template <typename InputIt>
DynamicArray(InputIt first, InputIt last)
    : _data_start(nullptr)
    , _data_finish(nullptr)
    , _heap_cap(nullptr)
{
    for (; first != last; ++first) {
        push_back(*first);
    }
}

The fill constructor pre-allocates storage then populates it:

DynamicArray(size_t count, const T& value)
    : _data_start(nullptr)
    , _data_finish(nullptr)
    , _heap_cap(nullptr)
{
    reserve(count);
    for (size_t i = 0; i < count; ++i) {
        push_back(value);
    }
}

Destruction and Cleanup

The destructor releases the contiguous heap block and poisons pointers to prevent use-after-free:

~DynamicArray() {
    delete[] _data_start;
    _data_start = _data_finish = _heap_cap = nullptr;
}

Deep Copy Semantics

When implementing the copy constructor, avoid std::memcpy for element transfer. While bitwise copying works for primitive types, it creates shallow copies for classes managing dynamic resources (like strings or nested containers), causing double-free errors during destruction.

Incorrect approach:

// Dangerous: performs shallow copy
memcpy(new_buffer, source._data_start, sizeof(T) * source.size());

Correct implementation using element-wise assignment:

DynamicArray(const DynamicArray& other)
    : _data_start(nullptr)
    , _data_finish(nullptr)
    , _heap_cap(nullptr)
{
    reserve(other.capacity());
    for (const auto& element : other) {
        push_back(element);
    }
}

Copy-and-Swap Idiom

Modern assignment operators leverage pass-by-value and non-throwing swaps for exception safety and simplicity:

DynamicArray& operator=(DynamicArray rhs) {
    swap(rhs);
    return *this;
}

void swap(DynamicArray& other) noexcept {
    using std::swap;
    swap(_data_start, other._data_start);
    swap(_data_finish, other._data_finish);
    swap(_heap_cap, other._heap_cap);
}

Capacity Management

Query functions calculate logical properties from pointer arithmetic:

size_t size() const { 
    return static_cast<size_t>(_data_finish - _data_start); 
}

size_t capacity() const { 
    return static_cast<size_t>(_heap_cap - _data_start); 
}

bool empty() const { 
    return _data_start == _data_finish; 
}

Storage Reallocation

When expanding capacity, preserve the element count before reallocating, as pointer arithemtic becomes invalid after the heap block moves:

void reserve(size_t new_cap) {
    if (new_cap <= capacity()) return;
    
    size_t current_size = size();
    T* new_block = new T[new_cap];
    
    if (_data_start) {
        for (size_t i = 0; i < current_size; ++i) {
            new_block[i] = _data_start[i];
        }
        delete[] _data_start;
    }
    
    _data_start = new_block;
    _data_finish = _data_start + current_size;
    _heap_cap = _data_start + new_cap;
}

Resizing Operations

resize adjusts the logical element count, truncating or expanding as needed:

void resize(size_t new_size, const T& value = T()) {
    if (new_size < size()) {
        _data_finish = _data_start + new_size;
    } else {
        if (new_size > capacity()) {
            reserve(new_size);
        }
        while (_data_finish < _data_start + new_size) {
            *_data_finish = value;
            ++_data_finish;
        }
    }
}

Stack Operations

Appending requires growth logic that handles the zero-capacity edge case:

void push_back(const T& value) {
    if (_data_finish == _heap_cap) {
        size_t new_capacity = (capacity() == 0) ? 2 : capacity() * 2;
        reserve(new_capacity);
    }
    *_data_finish = value;
    ++_data_finish;
}

Removal simply retreats the finish pointer with out deallocating storage:

void pop_back() {
    assert(_data_start < _data_finish);
    --_data_finish;
}

Arbitrary Insertion and Deletion

Inserting at arbitrary positions invalidates iterators if reallocation occurs. Preserve the logical offset to recover the position pointer after potential buffer relocation:

iterator insert(iterator pos, const T& value) {
    size_t offset = static_cast<size_t>(pos - _data_start);
    
    if (_data_finish == _heap_cap) {
        size_t new_cap = (capacity() == 0) ? 1 : capacity() * 2;
        reserve(new_cap);
        pos = _data_start + offset;
    }
    
    iterator current = _data_finish;
    while (current > pos) {
        *current = *(current - 1);
        --current;
    }
    *pos = value;
    ++_data_finish;
    return pos;
}

iterator erase(iterator pos) {
    assert(pos < _data_finish);
    iterator current = pos;
    while (current < _data_finish - 1) {
        *current = *(current + 1);
        ++current;
    }
    --_data_finish;
    return pos;
}

Note that these operations require O(N) element shifts, making them inefficient for frequent modifications at arbitrary positions.

Iterator Interface

Raw pointers serve as random-access iterators:

iterator begin() { return _data_start; }
iterator end() { return _data_finish; }
const_iterator begin() const { return _data_start; }
const_iterator end() const { return _data_finish; }

Element Access

Bounds-checked indexing ensures safe access:

T& operator[](size_t index) {
    assert(index < size());
    return _data_start[index];
}

const T& operator[](size_t index) const {
    assert(index < size());
    return _data_start[index];
}
Tags: C++

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.