I'm new to C++ (approx. 3 months) and have just read Stroustrup's Programming Principles and Practice. Previously used Python a lot.
As an exercise I tried to implement a custom container, with the following requirements:
Data Stored continuously in memory as an array (No linked lists or trees)
Dynamically Allocated (std::allocator)
Inherently Sorted, but accepting a custom functor to sort the elements.
Insert elements in right sorted place;
Allows duplicate elements, although easy to deduplicate;
Basically a mix of std::vector
and std::set
, but as I am using an array to store elements it will be more expensive to insert elements, but quick to search using binary_search, etc... Just an exercise, not trying to reinvent the wheel.
That said, i have the following code that works great so far.
I can make SortedVectors with types int
, double
, char
, bool
, string literals (const char*
) and even vector of vectors (ex.: SortedVector< SortedVector<int> >
) to work as matrix.
(However, I cannot use std::string
or other complicated types in the container, or I get segfaults.)
Is the design right or should I make it different?
Here's the code I would greatly appreciate to have reviewed.
#ifndef _SORTED_VECTOR_HPP
#define _SORTED_VECTOR_HPP
#include <functional>
#include <memory>
#include <initializer_list>
#include <algorithm>
#include <iostream>
namespace homebrew {
struct out_of_range {}; //exceptions
struct elem_not_found {};
// SortedVector<type, sort_functor, allocator>
template<typename T, typename S = std::less<T>, typename A =std::allocator<T>>
class SortedVector {
public:
using size_type = unsigned long;
using value_type = T;
using iterator = T*;
using const_iterator = const T*;
explicit SortedVector(size_type s, T val)
: sz{s}, elem{alloc.allocate(s)}, space(s)
{
for (size_type i=0; i<sz; ++i) alloc.construct(&elem[i], val);
}
explicit SortedVector(size_type s)
: sz{0}, elem{alloc.allocate(s)}, space{s} {}
SortedVector() // default constructor
: sz{0}, elem{nullptr}, space{0} {}
SortedVector(std::initializer_list<T> lst) // Initializer list constructor
: sz{lst.size()}, elem{alloc.allocate(lst.size())}, space{lst.size()}
{
std::copy(lst.begin(), lst.end(), elem);
std::sort(elem, elem+sz, functor);
}
SortedVector(const SortedVector& arg) // copy constructor (SortedVector v1 = v) -> need deep copy
: sz{arg.sz}, elem{alloc.allocate(arg.sz)}, space{arg.sz}
{
for (size_type i=0; i<arg.sz; ++i) alloc.construct(&elem[i], arg.elem[i]);
}
SortedVector& operator=(const SortedVector& arg) //copy assignment -> need deep copy
{
if (this == &arg) return *this; //self assignment, return
if (arg.sz <= space) { //enough space, no need for reallocation
for (size_type i=0; i<arg.sz; ++i) alloc.construct(&elem[i], arg.elem[i]);
sz = arg.sz;
return *this;
}
T* p = alloc.allocate(arg.sz);
for (size_type i=0; i<arg.sz; ++i) alloc.construct(&p[i], arg.elem[i]);
for (size_type i=0; i<sz; ++i) alloc.destroy(&elem[i]); //deallocate
alloc.deallocate(elem, sz); //old space
elem = p;
space = sz = arg.sz;
return *this;
}
SortedVector(SortedVector&& arg) //move constructor
: sz{arg.sz}, elem{arg.elem}, space{arg.sz} // move elem to new owner
{
arg.sz = 0; //empty the source
arg.elem = nullptr;
}
SortedVector& operator=(SortedVector&& arg) //move assignment (move arg to this vector)
{
for (size_type i=0; i<sz; ++i) alloc.destroy(&elem[i]);
alloc.deallocate(elem, sz);
elem = arg.elem;
space = sz = arg.sz;
arg.elem = nullptr;
arg.sz = arg.space = 0;
return *this;
}
~SortedVector() //destructor
{
for (size_type i=0; i<sz; ++i) alloc.destroy(&elem[i]);
alloc.deallocate(elem, sz);
}
// Checked access (cannot assign - would loose sort)
const T& at(size_type n) const
{
if (n<0 || n>=sz) throw out_of_range();
return elem[n];
}
//unchecked access (only access, cannot assign)
const T& operator[](size_type n) const { return elem[n]; }
size_type size() const noexcept { return sz; }
size_type capacity() const noexcept { return space; }
value_type* data() noexcept { return elem; }
const value_type* data() const noexcept { return elem; }
bool empty() const noexcept { return sz == 0 ? true: false; }
const iterator begin() const noexcept { return elem; }
const iterator end() const noexcept { return elem == nullptr ? nullptr: elem + sz;}
const iterator back() const noexcept { return elem==nullptr ? nullptr: elem + sz - 1;}
const iterator front() const noexcept { return elem==nullptr ? nullptr: elem;}
void reserve(size_type newalloc) //change memory size (never decrease)
{
if (newalloc <= space) return;
T* p = alloc.allocate(newalloc);
for (size_type i=0; i<sz; ++i) alloc.construct(&p[i], elem[i]);
for (size_type i=0; i<sz; ++i) alloc.destroy(&elem[i]);
alloc.deallocate(elem, sz);
elem = p;
space = newalloc;
}
//You insert, but the position is determined by the functor sort
void insert(const T& val)
{
if (space == 0) reserve(12);
else if(space <= sz) reserve(space * 2);
auto lowerBound = std::lower_bound(elem, elem+sz, val, functor);
//first copy last element into uninitialized space
alloc.construct(elem+sz, *back());
++sz;
for (auto pos = end()-1; pos !=lowerBound; --pos) //shift
*pos = *(pos - 1);
alloc.construct(lowerBound, val); //insert the val
}
void insert(std::initializer_list<T> lst)
{
for (auto& obj: lst) this->insert(obj);
}
void insert_unique(const T& val)
{
if (!contain(val)) insert(val);
}
void insert_unique(std::initializer_list<T> lst)
{
for (auto& obj: lst) {
if (!this->contain(obj)) this->insert(obj);
}
}
void erase(const T& val)
{
auto pp = std::upper_bound(elem, elem+sz, val, functor);
if (pp == begin()) throw elem_not_found();
else if (pp == end()) {
if (val == *back()) {
alloc.destroy(back());
--sz;
return;
}else throw elem_not_found();
}
alloc.destroy(pp-1);
for (auto pos = pp-1; pos != end()-1; ++pos)
*pos = *(pos+1);
--sz;
}
void erase(std::initializer_list<T> lst)
{
for (auto& obj: lst) this->erase(obj);
}
void erase(iterator position)
{
alloc.destroy(position);
while (position != end()-1) {
*position = *(position+1);
++position;
}
--sz;
}
void erase(iterator first, iterator last) //erase a range of elements
{
int shift = 0;
for (iterator i = first; i != last; ++i) {
alloc.destroy(i);
++shift;
}
for (iterator i = first; i != end()-shift; ++i) {
*i = *(i + shift);
}
sz -= shift;
}
void clear()
{
for (size_type i=0; i<sz; ++i) alloc.destroy(&elem[i]); //destroy but keep memory allocated
sz = 0;
}
bool contain(const T& arg)
{
return std::binary_search(elem, elem+sz, arg, functor);
}
size_type count(const T& arg)
{
size_type temp = 0;
for (size_type i=0; i<sz; ++i) {
if (arg == elem[i]) ++temp;
}
return temp;
}
void deduplicate()
{
//erase(std::unique(begin(), end()), end()); -> Faster??
for (size_type i=0; i<sz; ++i) {
while (count(elem[i]) > 1) {
erase(elem[i]);
}
}
}
SortedVector<T, S, A>& operator+=(const SortedVector<T, S, A>& rhs)
{
for (auto& obj: rhs) this->insert(obj);
return *this;
}
SortedVector<T, S, A>& operator-=(const SortedVector<T, S, A>& rhs)
{
for (auto& obj: rhs) this->erase(obj); //Beware it throws if you try to remove
return *this; // a non-existent element
}
private:
size_type sz;
size_type space;
A alloc;
T* elem;
S functor;
}; // class SortedVector
template<typename T, typename S, typename A>
inline SortedVector<T, S, A> operator+(SortedVector<T, S, A> lhs, const SortedVector<T, S, A>& rhs)
{
lhs += rhs;
return lhs;
}
template<typename T, typename S, typename A>
inline SortedVector<T, S, A> operator-(SortedVector<T, S, A> lhs, const SortedVector<T, S, A>& rhs)
{
lhs -= rhs;
return lhs;
}
template<typename T, typename S, typename A> // to cout and easy my debugging with std types
std::ostream& operator<<(std::ostream& os, const SortedVector<T, S, A>& vec)
{
os << "[";
for (auto i = vec.begin(); i != vec.end(); ++i) {
os << *i;
if (i == vec.end() - 1) { os << "]"; } else { os << ", "; } // x ? y:z operations didnt work. why?
}
os << "\n";
return os;
}
} //namespace homebrew
#endif //_SORTED_VECTOR_HPP