I have implemented a class Vector to learn about data structures and algorithms and how to implement them using C++. I have yet to implement some std::vector functionalities e.g. custom allocator template parameter, iterator/const_iterator classes.
I'm learning C++ through a C++11 book and trying to learn C++14 and C++17 best practices on the fly. I would appreciate any advice on how to improve or make it more compatible with modern best practices. I'm using g++ compiler with -std=c++17 flag.
One specific issue that I was in doubt was about the move assignment operator. I saw some code around the internet using a unified assignment operator for both copy and move assignment operators using swap, but this link made me think that it wasn't the best way to do it. Any thoughts?
Vector.h
namespace algorithms
{
template<typename T>
class Vector
{
public:
using size_type = std::size_t;
using iterator = T*;
using const_iterator = const T*;
using reference = T&;
using const_reference = const T&;
// Constructors and Destructor
Vector();
Vector(size_type initial_size, const T& value);
explicit Vector(size_type initial_size);
Vector(std::initializer_list<T> initializer);
Vector(const Vector<T>& vector);
Vector(Vector<T>&& vector) noexcept;
Vector& operator=(const Vector<T>& vector);
Vector& operator=(Vector<T>&& vector) noexcept;
~Vector();
// Iterators
/// TODO: Replace pointers by an iterator class
iterator begin() noexcept;
const_iterator begin() const noexcept;
const_iterator cbegin() const noexcept;
iterator end() noexcept;
const_iterator end() const noexcept;
const_iterator cend() const noexcept;
// Capacity
size_type size() const noexcept;
bool empty() const noexcept;
size_type capacity() const noexcept;
void reserve(size_type new_capacity);
void resize(size_type new_size, const T& value);
void resize(size_type new_size);
void shrink_to_fit();
// Modifiers
template<typename... Args>
reference emplace_back(Args&&... args);
template<typename... Args>
iterator emplace(iterator position, Args&&... args);
void push_back(const T& value);
void push_back(T&& value);
void pop_back();
iterator erase(iterator position);
iterator erase(iterator first, iterator last);
void clear() noexcept;
void swap(Vector<T>& vector) noexcept;
// Accessors
reference operator[](size_type index);
const_reference operator[](size_type index) const;
reference at(size_type index);
const_reference at(size_type index) const;
reference back();
const_reference back() const;
private:
using Alloc = std::allocator<T>;
Alloc allocator;
T* dynamic_array;
T* end_position; // points to one past the last constructed element in the array
T* capacity_limit; // points to one past the end of the array
void reallocate(size_type new_capacity);
void reallocate_if_full();
void deallocate();
void allocate_and_copy(const_iterator begin, const_iterator end);
};
// Non-member swap
template<typename T>
void swap(Vector<T>& left, Vector<T>& right);
}
Vector.inl
namespace algorithms
{
// Constructors
template<typename T>
Vector<T>::Vector(): dynamic_array(nullptr), end_position(nullptr), capacity_limit(nullptr)
{}
template<typename T>
Vector<T>::Vector(size_type initial_size, const T& value)
{
dynamic_array = allocator.allocate(initial_size);
end_position = std::uninitialized_fill_n(dynamic_array, initial_size, value);
capacity_limit = dynamic_array + initial_size;
}
template<typename T>
Vector<T>::Vector(size_type initial_size): Vector<T>(initial_size, T())
{}
template<typename T>
Vector<T>::Vector(std::initializer_list<T> initializer)
{
allocate_and_copy(initializer.begin(), initializer.end());
}
template<typename T>
Vector<T>::Vector(const Vector<T>& vector)
{
allocate_and_copy(vector.cbegin(), vector.cend());
}
template<typename T>
Vector<T>::Vector(Vector<T>&& vector) noexcept:
dynamic_array(vector.dynamic_array), end_position(vector.end_position), capacity_limit(vector.capacity_limit)
{
vector.dynamic_array = nullptr;
vector.end_position = nullptr;
vector.capacity_limit = nullptr;
}
template<typename T>
Vector<T>& Vector<T>::operator=(const Vector<T>& vector)
{
Vector<T> temp(vector);
swap(temp);
return *this;
}
template<typename T>
Vector<T>& Vector<T>::operator=(Vector<T>&& vector) noexcept
{
if (this != &vector) // protection against self-assignment
{
deallocate();
dynamic_array = vector.dynamic_array;
end_position = vector.end_position;
capacity_limit = vector.capacity_limit;
vector.dynamic_array = nullptr;
vector.end_position = nullptr;
vector.capacity_limit = nullptr;
}
return *this;
}
template<typename T>
Vector<T>::~Vector()
{
deallocate();
}
// Iterators
template<typename T>
typename Vector<T>::iterator Vector<T>::begin() noexcept
{
return dynamic_array;
}
template<typename T>
typename Vector<T>::const_iterator Vector<T>::begin() const noexcept
{
return dynamic_array;
}
template<typename T>
typename Vector<T>::const_iterator Vector<T>::cbegin() const noexcept
{
return dynamic_array;
}
template<typename T>
typename Vector<T>::iterator Vector<T>::end() noexcept
{
return end_position;
}
template<typename T>
typename Vector<T>::const_iterator Vector<T>::end() const noexcept
{
return end_position;
}
template<typename T>
typename Vector<T>::const_iterator Vector<T>::cend() const noexcept
{
return end_position;
}
// Capacity
template<typename T>
typename Vector<T>::size_type Vector<T>::size() const noexcept
{
return static_cast<size_type>(end_position - dynamic_array);
}
template<typename T>
bool Vector<T>::empty() const noexcept
{
return size() == 0;
}
template<typename T>
typename Vector<T>::size_type Vector<T>::capacity() const noexcept
{
return static_cast<size_type>(capacity_limit - dynamic_array);
}
template<typename T>
void Vector<T>::reserve(size_type new_capacity)
{
if (new_capacity <= capacity())
{
return;
}
reallocate(new_capacity);
}
template<typename T>
void Vector<T>::resize(size_type new_size, const T& value)
{
if (new_size > capacity())
{
reallocate(2 * new_size);
end_position = std::uninitialized_fill_n(end_position, new_size - size(), value);
}
else if (new_size > size())
{
end_position = std::uninitialized_fill_n(end_position, new_size - size(), value);
}
else if (new_size < size())
{
for (size_type i = 0; i < size() - new_size; ++i)
{
--end_position;
std::allocator_traits<Alloc>::destroy(allocator, end_position);
}
}
}
template<typename T>
void Vector<T>::resize(size_type new_size)
{
resize(new_size, T());
}
template<typename T>
void Vector<T>::shrink_to_fit()
{
reallocate(size());
}
// Modifiers
template<typename T>
template<typename... Args>
typename Vector<T>::reference Vector<T>::emplace_back(Args&&... args)
{
reallocate_if_full();
std::allocator_traits<Alloc>::construct(allocator, end_position, std::forward<Args>(args)...);
++end_position;
}
template<typename T>
template<typename... Args>
typename Vector<T>::iterator Vector<T>::emplace(iterator position, Args&&... args)
{
const size_type distance = std::distance(begin(), position);
if (position == end_position)
{
emplace_back(std::forward<Args>(args)...);
}
else
{
reallocate_if_full();
std::move_backward(begin() + distance, end_position, end_position + 1);
std::allocator_traits<Alloc>::construct(allocator, begin() + distance, std::forward<Args>(args)...);
++end_position;
}
return begin() + distance;
}
template<typename T>
void Vector<T>::push_back(const T& value)
{
emplace_back(value);
}
template<typename T>
void Vector<T>::push_back(T&& value)
{
emplace_back(std::move(value));
}
template<typename T>
void Vector<T>::pop_back()
{
--end_position;
std::allocator_traits<Alloc>::destroy(allocator, end_position);
}
template<typename T>
typename Vector<T>::iterator Vector<T>::erase(iterator position)
{
std::move(position + 1, end(), position);
--end_position;
std::allocator_traits<Alloc>::destroy(allocator, end_position);
return position;
}
template<typename T>
typename Vector<T>::iterator Vector<T>::erase(iterator first, iterator last)
{
if (first == last)
{
return begin();
}
auto new_end_position = std::move(last, end(), first);
for (auto iterator = new_end_position; iterator != end_position; ++iterator)
{
std::allocator_traits<Alloc>::destroy(allocator, iterator);
}
end_position = new_end_position;
return first;
}
template<typename T>
void Vector<T>::clear() noexcept
{
deallocate();
dynamic_array = nullptr;
end_position = nullptr;
capacity_limit = nullptr;
}
template<typename T>
void Vector<T>::swap(Vector<T>& vector) noexcept
{
using std::swap;
swap(this->dynamic_array, vector.dynamic_array);
swap(this->end_position, vector.end_position);
swap(this->capacity_limit, vector.capacity_limit);
}
// Accessors
template<typename T>
typename Vector<T>::reference Vector<T>::operator[](size_type index)
{
return dynamic_array[index];
}
template<typename T>
typename Vector<T>::const_reference Vector<T>::operator[](size_type index) const
{
return dynamic_array[index];
}
template<typename T>
typename Vector<T>::reference Vector<T>::at(size_type index)
{
if (index < 0 || index >= size())
{
throw std::out_of_range("Invalid index");
}
return dynamic_array[index];
}
template<typename T>
typename Vector<T>::const_reference Vector<T>::at(size_type index) const
{
if (index < 0 || index >= size())
{
throw std::out_of_range("Invalid index");
}
return dynamic_array[index];
}
template<typename T>
typename Vector<T>::reference Vector<T>::back()
{
return dynamic_array[size() - 1];
}
template<typename T>
typename Vector<T>::const_reference Vector<T>::back() const
{
return dynamic_array[size() - 1];
}
// Private
template<typename T>
void Vector<T>::reallocate(size_type new_capacity)
{
auto new_array = allocator.allocate(new_capacity);
auto new_end_position = std::uninitialized_copy(std::make_move_iterator(begin()), std::make_move_iterator(end()), new_array);
deallocate();
dynamic_array = new_array;
end_position = new_end_position;
capacity_limit = dynamic_array + new_capacity;
}
template<typename T>
void Vector<T>::reallocate_if_full()
{
if (size() == capacity())
{
size_type new_capacity = (size() != 0) ? 2 * size() : 1;
reallocate(new_capacity);
}
}
template<typename T>
void Vector<T>::deallocate()
{
if (dynamic_array)
{
std::for_each(dynamic_array, end_position,
[&allocator = allocator](T& value) { std::allocator_traits<Alloc>::destroy(allocator, &value); });
allocator.deallocate(dynamic_array, capacity_limit - dynamic_array);
}
}
template<typename T>
void Vector<T>::allocate_and_copy(const_iterator begin, const_iterator end)
{
size_type new_capacity = end - begin;
dynamic_array = allocator.allocate(new_capacity);
end_position = std::uninitialized_copy(begin, end, dynamic_array);
capacity_limit = end_position;
}
// Non-member swap function
template<typename T>
void swap(Vector<T>& left, Vector<T>& right)
{
left.swap(right);
}
}