I want to improve my algorithms and data structures in C++ understanding and also learn how the standard library containers are implemented. This resizing_array is based on the std::vector.
My questions are:
Are there any memory allocation/deallocation errors in this implementation?
Is the design ok? Can it be improved?
Any bugs?
Any feedback not mentioned above would be interesting.
resizing_array.hpp:
#ifndef RESIZING_ARRAY_
#define RESIZING_ARRAY_
/*
Useful resources:
https://en.cppreference.com/w/cpp/container/vector
https://www.cs.odu.edu/~zeil/cs361/sum18/Public/vectorImpl/index.html
https://www.cs.odu.edu/~zeil/references/cpp_ref_draft_nov97/lib-containers.html
Operation Speed
vector() O(1)
vector(n, x) O(n) // not implemented
size() O(1)
v[ i ] O(1)
push_back(x) O(1)
pop_back O(1)
insert O(size()) // not implemented
erase O(size()) // not implemented
front, back O(1) // not implemented
*/
#include <iterator>
#include <algorithm>
#include <initializer_list>
#include <iostream> // debug output
namespace play {
template< typename T >
class resizing_array {
public:
template< typename input_iterator >
resizing_array(input_iterator first, input_iterator last) {
size_ = std::distance(first, last);
capacity_ = size_;
array_ = nullptr;
resize_array(capacity_);
std::copy(first, last, array_);
}
resizing_array(std::initializer_list<T> init) : size_(init.size()), capacity_(init.size()), array_(nullptr) {
size_ = init.size();
capacity_ = size_;
resize_array(capacity_);
std::copy(init.begin(), init.end(), array_);
}
resizing_array() : size_(0), capacity_(8), array_(nullptr) {
resize_array(8);
std::cout << "allocating array at address: " << array_ << std::endl;
}
resizing_array(const resizing_array& other) : size_(other.size()), capacity_(other.capacity()), array_(nullptr) {
resize_array(capacity_);
std::cout << "copy ctor allocated array at address: " << array_ << std::endl;
std::copy(other.begin(), other.end(), array_);
}
resizing_array& operator=(const resizing_array& other) {
if (this != &other) {
size_ = other.size();
capacity_ = other.capacity();
free(array_);
array_ = nullptr;
resize_array(capacity_);
std::cout << "op= allocated array at address: " << array_ << std::endl;
std::copy(other.begin(), other.end(), array_);
}
return *this;
}
resizing_array(resizing_array&& other) : size_(other.size()), capacity_(other.capacity()), array_(other.begin()) {
other.size_ = 0;
other.capacity_ = 0;
other.array_ = nullptr;
}
resizing_array& operator=(resizing_array&& other) {
free(array_);
array_ = other.array_;
size_ = other.size();
capacity_ = other.capacity();
other.array_ = nullptr;
other.size_ = 0;
other.capacity_ = 0;
}
~resizing_array() {
std::cout << "deallocating array at address: " << array_ << std::endl;
free(array_);
}
void push_back(const T& v) {
if (size_ == capacity_) {
resize_array(size_ * 2);
capacity_ = size_ * 2;
}
array_[size_++] = v;
}
void pop_back() {
--size_;
}
size_t size() const { return size_; }
T& operator[](size_t index) {
return array_[index];
}
const T& operator[](size_t index) const {
return array_[index];
}
T* begin() {
return array_;
}
T* end() {
return array_ + capacity_;
}
const T* begin() const {
return array_;
}
const T* end() const {
return array_ + capacity_;
}
size_t capacity() const {
return capacity_;
}
private:
bool resize_array(size_t new_size) {
void* new_pointer = realloc(array_, new_size * sizeof(T));
if (new_pointer) {
array_ = static_cast<T*>(new_pointer);
std::cout << "array at address: " << array_ << " size increased to: " << new_size << std::endl;
return true;
}
else {
return false;
}
}
size_t size_;
size_t capacity_;
T* array_;
};
} // end of namespace play
#endif // RESIZING_ARRAY_
main.cpp to exercise:
#include "resizing_array.hpp"
#include <cassert>
using namespace play;
// to test move constructor
resizing_array<int> fill(std::istream& is) {
std::cout << "Enter your list of integers, ctrl-D to finish: ";
resizing_array<int> ra;
for (int v; is >> v; )
ra.push_back(v);
return ra;
}
int main() {
resizing_array<int> ra;
// Can it grow?
for (size_t i = 0; i < 100; ++i) {
ra.push_back(i);
}
// can we read elements by [index]?
std::cout << "ra[0]=" << ra[0] << std::endl;
std::cout << "ra[1]=" << ra[1] << std::endl;
std::cout << "ra[2]=" << ra[2] << std::endl;
std::cout << "ra[3]=" << ra[3] << std::endl;
std::cout << "ra[4]=" << ra[4] << std::endl;
// can we write a value by [index]?
ra[3] = 10;
std::cout << "ra[3] is now =" << ra[3] << std::endl;
// can we remove elements?
std::cout << "current size of resizing array is: " << ra.size() << " and last element: " << ra[ra.size() - 1] << std::endl;
ra.pop_back();
std::cout << "size of resizing array after pop() is: " << ra.size() << " and last element: " << ra[ra.size() - 1] << std::endl;
// copy construction works?
resizing_array<int> ra2(ra);
std::cout << "current size of resizing array 2 is: " << ra2.size() << " and last element: " << ra2[ra2.size() - 1] << std::endl;
// create a new resizing_array from begin and end iterator constructor
const int raw_array[]{ 1,2,3,4,5};
size_t size = sizeof(raw_array) / sizeof(raw_array[0]);
resizing_array<int> ra3(raw_array, raw_array + size);
assert(ra3.size() == 5);
assert(ra3[0] == raw_array[0]);
assert(ra3[1] == raw_array[1]);
assert(ra3[2] == raw_array[2]);
assert(ra3[3] == raw_array[3]);
assert(ra3[4] == raw_array[4]);
// create a new resizing array from an initialisation_list
resizing_array<int> ra4{1,2,3,4,5};
assert(ra4[0] == 1);
assert(ra4[1] == 2);
assert(ra4[2] == 3);
assert(ra4[3] == 4);
assert(ra4[4] == 5);
// assignment operator test
// will crash if haven't implemented operator=
ra4 = ra3;
assert(ra4[ra4.size() - 1] == ra3[ra3.size() - 1]);
// move constructor and move assignment test
resizing_array<int> ra5 = fill(std::cin);
}
std::vector
is not as simple as it might appears.malloc
/free
doesn't construct/destruct objects. It might not be a problem forint
which has a noop constructor/destructor, but you should have issue with more complex types (resizing_array<resizing_array<int>>
would leak). \$\endgroup\$realloc
cannot be used for non-trivially-copiable types. so either have (compile-time) branches depending of property ofT
, and use the "inefficient" manner for complex types. As I saidstd::vector
is complex (And I even think it is not pedantically possible to implement it without UB (because of pointer arithmetic on non-array)), so it mainly depend on which contracts you want to fulfill and what you want to ignore, which type are allowed (as non default-constructible type are problematic for you), ... \$\endgroup\$<cstdlib>
for a definition ofstd::realloc
. And have misspeltstd::size_t
throughout. \$\endgroup\$