This post describes my minimum working implementation of a Vector class in C++, similar to the one from standard library’s std::vector but with only required functionality. The objective is to implement a generic array of dynamic size, i.e. which grows in size as new data gets added to it. We need at least 2 variables to keep track of the capacity of the array and the current index (which is also the size of the data available in the array). We also need a way to access these variables.

class Vector {
    size_t capacity_ = 0;   // current memory capacity
    size_t curr_idx_ = 0;   // current vector size (same as numel)

    public:
        size_t size() const {return curr_idx_;} // returns current size of our vector
        size_t capacity() const {return capacity_;} // returns current capacity of our vector
};

Indexing the vector

We store a pointer to the first element of the array. With this, we can access every element of the array up to it’s size. The pointer to the first element will be a private member. So, we also need a way to access the vector’s elements for read and write purposes.

In order to access the vector’s individual elements, we need a method to index the vector. This indexing method will be used for both reading and writing the vector’s elements. We define an operator[] which returns the element’s reference as follows

template<class T> class Vector {

    T* vector_ = nullptr;   // pointer to first data element
    size_t capacity_ = 0;   // current memory capacity
    size_t curr_idx_ = 0;   // current vector size (same as numel)

    public:
        // ... //same as above

        // Element read/write access
        T& operator[](const size_t index); // return element reference at index
};

//// Definitions

// Element read/write access
template<class T>
T& Vector<T>::operator[](const size_t index)
{
    if (index >= curr_idx_)
        throw std::invalid_argument("Index must be less than vector's size");
    
    return vector_[index];
}

We need to allocate a dynamic memory using new[], which reserves the input amount of memory for our array. . Dynamic allocation of memory means we also need to free the memory manually upon destruction.

template<class T>  class Vector {
    T* vector_ = nullptr;   // pointer to first data element
    size_t capacity_ = 0;   // current memory capacity
    size_t curr_idx_ = 0;   // current vector size (same as numel)

    public:
        // Constructors 
        Vector() = default; // default constructor

        // Destructor
        ~Vector() {delete[] vector_;}
};

Rule of 0/3/5: Defining copy constructor and copy assignment

Since we are explicitly defining a destructor for manual memory management, we also need to define the copy constructor and the copy assignment by the rule of 0/3.

template<class T>  class Vector {
    T* vector_ = nullptr;   // pointer to first data element
    size_t capacity_ = 0;   // current memory capacity
    size_t curr_idx_ = 0;   // current vector size (same as numel)

    public:
        // Constructors 
        Vector() = default; // default constructor
        Vector(const Vector<T>& another_vector);    // copy constructor
        Vector<T>& operator=(const Vector<T> &);    // copy assignment

        // Destructor
        ~Vector() {delete[] vector_;}
};

The copy constructor is used to copy an object of the same type. In our case, we need the copy constructor to copy another vector’s elements. Note that when not defined explicitly, the compiler defines a default copy constructor which does not do what we want. So, it is necessary to define a copy constructor

// Declaration same as before

//// Definitions

// Copy constructor
template<class T>
Vector<T>::Vector(const Vector<T>& another_vector)
{
    delete[] vector_;   // Delete before copying everything from another vector

    // Copy everything from another vector
    curr_idx_ = another_vector.size();
    capacity_ = another_vector.capacity();
    vector_ = new T[capacity_];
    for (size_t i=0; i < capacity_; ++i)
        vector_[i] = another_vector[i];
}

The copy assignment is similar to the copy constructor but is called when the = operator is used, for e.g. Vector vector = another_vector;. The only difference here will be that the copy assignment will return the pointer to the object while the copy constructor doesn’t have to return anything.

// Declaration same as before

//// Definitions

// Copy assignment
template<class T>
Vector<T>& Vector<T>::operator=(const Vector<T>& another_vector)
{
    delete[] vector_;   // Delete before copying everything from another vector

    // Copy everything from another vector
    curr_idx_ = another_vector.size();
    capacity_ = another_vector.capacity();
    vector_ = new T[capacity_];
    for (size_t i=0; i < capacity_; ++i)
        vector_[i] = another_vector[i];

    return *this;
}

Adding and removing elements from the vector

We need a way to add elements to our vector. We can define an additional constructor (apart from the default one), which takes capacity as input and allocates memory of the given capacity. We can further extend this constructor to initialize our vector with a default value.

template<class T>  class Vector {
    T* vector_ = nullptr;   // pointer to first data element
    size_t capacity_ = 0;   // current memory capacity
    size_t curr_idx_ = 0;   // current vector size (same as numel)

    public:
        // Constructors 
        Vector() = default; // default constructor
        Vector(const Vector<T>& another_vector);    // copy constructor
        Vector<T>& operator=(const Vector<T> &);    // copy assignment
        Vector(size_t capacity, T initial = T{});   // constructor based on capacity and a default value

        // Destructor
        ~Vector() {delete[] vector_;}
};

//// Definitions

template<class T>
Vector<T>::Vector(size_t capacity, T initial): capacity_{capacity},
                                 curr_idx_{capacity},
                                 vector_{new T[capacity]{}} // allocate stack and store its pointer
{
    for (size_t i=0; i < capacity; ++i)
        vector_[i] = initial;   // initialize
}

The above constructor can be called as follows

Vector<int> vector(10);  // initializes a vector with capacity 10
Vector<int> vector_ones(10, 1)   // initializes with an initial value

At this point, a minimum working example of a static array is complete. If we also need to make our vector dynamic, we need a way to increase the vector’s capacity if needed. This will be useful if we want to add elements to the array after we initialize it.

Push back and pop methods

Since the goal of our array is to be dynamic, we also need ways to add and remove elements from it after initialization. To add and remove elements from our vector, we define emplace_back and pop methods respectively. The array also needs to increase its capacity if it needs to. For this, we define a private reserve method, which reserves the input amount of memory.

template<class T> class Vector {
    public:
        // ... //same as above

        void emplace_back(const T& element);    // pass element by constant reference
        T pop();    // pops the last element

    private:
        // ... // same as above
        
        void reserve(const size_t capacity);
};

//// Definitions

// ... // same as above

template<class T>
void Vector<T>::emplace_back(const T& element)
{
    // If no cacacity, increase capacity
    if (curr_idx_ == capacity_)
    {
        if (capacity_ == 0) // handing initial when 
            reserve(8);
        else
            reserve(capacity_*2);
    }

    // Append an element to the array
    vector_[curr_idx_] = element;
    curr_idx_++;
}

template<class T>
T Vector<T>::pop()
{   
    if (curr_idx_ > 0)  // Nothing to pop otherwise
    {
        T to_return = vector_[curr_idx_-1]; // store return value before deleting
        // vector_[curr_idx_-1]->~T();         // delete from memory
        curr_idx_--;

        return to_return;
    }
    else
        throw std::out_of_range("Nothing to pop")
}


While appending an element to our vector, the emplace_back function will check if the maximum capacity of the array is reached. This is done by comparing the curr_idx_ (size) of our vector with it’s capacity_. If capacity is same as size, then we reserve more memory. We do this inside the reserve function as follows

// Memory allocation
template<class T>
inline void Vector<T>::reserve(const size_t capacity)
{
    // Handle case when given capacity is less than equal to size. (No need to reallocate)
    if (capacity > curr_idx_)
    {
        // Reserves memory of size capacity for the vector_
        T* temp = new T[capacity];

        // Move previous elements to this memory
        for (size_t i=0; i < capacity_; ++i)
            temp[i] = vector_[i];

        delete[] vector_; // Delete old vector
        capacity_ = capacity;
        vector_ = temp;     // Copy assignment
    }
}

This completes our minimal working example of a vector class. In the next tutorials, we will extend this class to have iterators, a print function and mathematical operators.

Note that this implementation is just a minimal working example and is mainly for learning and educational purposes. The standard library’s vector must be preferred in practice.


© Zeeshan Khan Suri,

If this article was helpful to you, consider citing

@misc{suri_cpp_vector_class_2021,
      title={A minimal working example of a custom Vector class in C\+\+},
      url={https://zshn25.github.io/c++-vector-mwe-tutorial/}, 
      journal={Curiosity}, 
      author={Suri, Zeeshan Khan}, 
      year={2021}, 
      month={June}}