This is a late followup to a previous question of mine on same subject.
Context:
I want to have a tool that allows to use all the C++ goodies (algorithms, for loop on containers, etc.) on a multidimensional contiguous array whose sizes are only known at run time. That immediately means that std::arrays
are off (compile time sizes), as are vectors of vectors (not contiguous data). That also means that the common solution multi_array(i, j, k)
is also off because of no natural iterators and direct members.
I ended with a bunch a classes:
- a full container class (MDynArray) with all copy/move semantics provided the underlying type has - in order to mimic standard containers, allocator is configurable
- a sub objects class (MSubArray) which accesses (read-write) the memory of its parent and tries to mimic the operations of a sequencial direct access standard container (
operator[]
and iterators) - iterator and const_iterator classes for MSubArray (MDynArray being a subclass of it)
a builder class (ArrayBuilder), because I want to be able to
- build a new dynamic array from scratch
- have it copy an existing raw array
- have it use (no copy here) an existing raw array taking or not ownership
and could not find a way to declare constructors or static factory methods for those different use cases
Code
Here is my current code (yes it is quite long...)
#include <type_traits>
#include <algorithm>
#include <exception>
#include <stdexcept>
#include <memory>
#include <utility>
namespace DynArray {
using std::allocator;
using std::allocator_traits;
//Forward declarations
template <class T, int dims, class Allocator>
class MDynIteratorBase;
template <class T, int dims, int cnst, class Allocator,
class U = typename std::conditional<cnst == 1,
const MSubArray<T, dims, Allocator>, MSubArray<T, dims, Allocator>>::type>
class MDynIterator;
// Base: contains the data and declares types
template <class T, int dims, class Allocator>
class MDynArrayBase {
public:
using value_type = T;
using allocator_type = Allocator;
using pointer = typename allocator_traits<Allocator>::pointer;
using const_pointer = typename allocator_traits<Allocator>::const_pointer;
using reference = T&;
using const_reference = const T&;
using size_type = typename allocator_traits<Allocator>::size_type;
using difference_type = typename allocator_traits<Allocator>::difference_type;
protected:
T* arr;
size_type *sizes;
size_type rowsize;
MDynArrayBase(T* arr, size_type* sizes, size_type rowsize)
: arr(arr), sizes(sizes), rowsize(rowsize) {}
public:
virtual ~MDynArrayBase() {}
};
// Sub array: all the logic of accesses but do not manage (de-/)allocation
template <class T, int dims, class Allocator>
class MSubArray : public MDynArrayBase<T, dims, Allocator> {
protected:
using MDynArrayBase<T, dims, Allocator>::arr;
using MDynArrayBase<T, dims, Allocator>::sizes;
using MDynArrayBase<T, dims, Allocator>::rowsize;
MSubArray(T* arr, size_type* sizes, size_type rowsize)
: MDynArrayBase<T, dims, Allocator>(arr, sizes, rowsize) {}
public:
using iterator = typename MDynIterator<T, dims-1, 0, Allocator>;
using const_iterator = typename MDynIterator<T, dims-1, 1, Allocator>;
// access to member
MSubArray<T, dims-1, Allocator> operator[] (size_type i) {
MSubArray<T, dims-1, Allocator> child(arr + rowsize * i,
sizes + 1, rowsize / sizes[1]);
return child;
}
const MSubArray<T, dims-1, Allocator> operator[] (size_type i) const {
MSubArray<T, dims-1, Allocator> child(arr + rowsize * i,
sizes + 1, rowsize / sizes[1]);
return child;
}
// access to internal data, arr and sizes and number of dimensions
size_type size(size_type i = 0) const {
if (i >= dims) {
throw std::out_of_range("Illegal dimension");
}
if (sizes == nullptr) return 0;
return sizes[i];
}
size_type tot_size() const {
if (sizes == nullptr) return 0;
return sizes[0] * rowsize;
}
T* data() {
return arr;
}
const T* data() const {
return arr;
}
constexpr int getdims() const {
return dims;
}
// iterators
iterator begin() {
return iterator(arr, sizes + 1,
rowsize / sizes[1]);
}
iterator end() {
iterator tmp = begin();
tmp += sizes[0];
return tmp;
}
const_iterator cbegin() const {
return const_iterator(arr, sizes + 1,
rowsize / sizes[1]);
}
const_iterator cend() const {
const_iterator tmp = cbegin();
tmp += sizes[0];
return tmp;
}
friend class MSubArray<T, dims + 1, Allocator>;
friend class iterator;
friend class const_iterator;
friend class MDynIteratorBase<T, dims, Allocator>;
};
// specialization for 1D: members are true T objects
template <class T, class Allocator>
class MSubArray<T, 1, Allocator> : public MDynArrayBase<T, 1, Allocator> {
protected:
using MDynArrayBase<T, 1, Allocator>::arr;
using MDynArrayBase<T, 1, Allocator>::sizes;
MSubArray(T* arr, size_type* sizes, size_type rowsize)
: MDynArrayBase<T, 1, Allocator>(arr, sizes, rowsize) {}
public:
using iterator = typename T*;
using const_iterator = typename const T*;
~MSubArray() {}
T& operator[] (size_type i) {
return arr[i];
}
const T& operator[] (size_type i) const {
return arr[i];
}
// same for size and arr, dims
size_t size(size_t i = 0) {
if (i != 0) {
throw std::out_of_range("Illegal dimension");
}
if (sizes == nullptr) return 0;
return sizes[0];
}
size_type tot_size() const {
if (sizes == nullptr) return 0;
return sizes[0];
}
T* data() {
return arr;
}
const T* data() const {
return arr;
}
constexpr int getdims() const {
return 1;
}
//iterators
iterator begin() {
return arr;
}
iterator end() {
return arr + sizes[0];
}
const_iterator cbegin() const {
return arr;
}
const_iterator cend() const {
return arr + sizes[0];
}
friend class MSubArray<T, 2, Allocator>;
friend class iterator;
friend class const_iterator;
friend class MDynIteratorBase<T, 1, Allocator>;
};
// forward declaration for the builder class
template <class T, class Allocator = std::allocator<T> >
class ArrayBuilder;
// Full array, must manage allocation/deallocation of resources
template <class T, int dims, class Allocator = allocator<T> >
class MDynArray : public MSubArray<T, dims, Allocator> {
using MSubArray<T, dims, Allocator>::arr;
using MSubArray<T, dims, Allocator>::sizes;
using MSubArray<T, dims, Allocator>::rowsize;
bool ownarr; // if true, arr have to be deleted
Allocator alloc; // internal allocator
// allocates a T array and optionaly copy-initializes its elements
static T* clone(T* src, size_type tot, Allocator alloc) {
T* dst = allocator_traits<Allocator>::rebind_traits<T>::allocate(
alloc, tot);
size_type i;
try {
if (src != nullptr) {
for (i = 0; i < tot; i++) {
allocator_traits<Allocator>::rebind_traits<T>::construct(
alloc, dst + i, src[i]);
}
}
else {
for (i = 0; i < tot; i++) {
allocator_traits<Allocator>::rebind_traits<T>::construct(
alloc, dst + i);
}
}
}
catch(std::exception &) {
while (i-- > 0) {
allocator_traits<Allocator>::rebind_traits<T>::destroy(
alloc, dst + i);
}
allocator_traits<Allocator>::rebind_traits<T>::deallocate(alloc,
dst, tot);
throw;
}
return dst;
}
MDynArray(T* arr, size_type* sizes, size_type rowsize, bool ownarr,
const Allocator& alloc)
: MSubArray<T, dims, Allocator>(arr, sizes, rowsize),
ownarr(ownarr), alloc(alloc) {}
public:
// copy/move ctors and assignment (rule of 5)
MDynArray(const MDynArray<T, dims, Allocator>& other)
: MSubArray(nullptr, nullptr, 0) {
alloc = other.alloc;
ownarr = true;
sizes = new size_type[dims];
std::copy(other.sizes, other.sizes + dims, sizes);
rowsize = other.rowsize;
try {
arr = clone(other.arr, rowsize * sizes[0], alloc);
}
catch (std::exception&) {
delete[] sizes;
}
}
MDynArray(MDynArray<T, dims, Allocator>&& other)
: MSubArray(nullptr, nullptr, 0), alloc(Allocator()), ownarr(false) {
swap(other);
}
MDynArray<T, dims, Allocator>& operator = (
const MDynArray<T, dims, Allocator>& other) {
MDynArray<T, dims, Allocator> tmp(other);
swap(tmp);
return *this;
}
MDynArray<T, dims, Allocator>& operator = (
MDynArray<T, dims, Allocator>&& other) {
swap(other);
return *this;
}
~MDynArray() {
if (ownarr) {
delete[] arr;
}
delete[] sizes;
}
void swap(MDynArray<T, dims, Allocator>& other) {
using std::swap;
swap(arr, other.arr);
swap(sizes, other.sizes);
swap(rowsize, other.rowsize);
swap(ownarr, other.ownarr);
swap(alloc, other.alloc);
}
friend class ArrayBuilder<T, Allocator>;
};
// auxilliary class to build new MDynArray objects, possibly copying
// moving (take ownership) or just using a pre-existing array
template <class T, class Allocator>
class ArrayBuilder {
public:
using size_type = typename allocator_traits<Allocator>::size_type;
private:
Allocator alloc;
template <class...U>
static size_type calc_size(size_type *sizes, size_type first,
U...others) {
if (sizes != nullptr) *sizes = first;
return first * calc_size(sizes + 1, others...);
}
static size_type calc_size(size_type *sizes, size_type first) {
if (sizes != nullptr) *sizes = first;
return first;
}
public:
ArrayBuilder(const Allocator& alloc = Allocator()) : alloc(alloc) {}
template <class T, class ...U>
MDynArray<T, sizeof...(U)+1, Allocator> dynUseArray(T* arr,
size_type first, U...others) {
constexpr size_t dims = sizeof...(U)+1;
size_type *sizes = new size_type[dims];
size_type tot = calc_size(sizes, first, others...);
size_type rowsize = tot / sizes[0];
return MDynArray<T, dims, Allocator>(arr, sizes, rowsize,
false, alloc);
}
template <class T, class ...U>
typename std::enable_if<std::is_copy_constructible<T>::value,
MDynArray<T, sizeof...(U)+1, Allocator>>::type
dynCopyArray(T* arr, size_type first, U...others) {
constexpr size_t dims = sizeof...(U)+1;
size_type *sizes = new size_type[dims];
size_type tot = calc_size(sizes, first, others...);
T* dst;
try {
dst = MDynArray<T, dims, Allocator>::clone(arr, tot, alloc);
}
catch (std::exception&) {
delete[] sizes;
throw;
}
return MDynArray<T, sizeof...(U)+1, Allocator>(dst, sizes,
tot / sizes[0], true, alloc);
}
template <class ...U>
MDynArray<T, sizeof...(U)+1, Allocator>
dynBuildArray(size_type first, U...others) {
constexpr size_t dims = sizeof...(U)+1;
size_type *sizes = new size_type[dims];
size_type tot = calc_size(sizes, first, others...);
T* dst;
try {
dst = MDynArray<T, dims, Allocator>::clone(nullptr, tot, alloc);
}
catch (std::exception&) {
delete[] sizes;
throw;
}
return MDynArray<T, sizeof...(U)+1, Allocator>(dst, sizes,
tot / sizes[0], true, alloc);
}
template <class T, class ...U>
MDynArray<T, sizeof...(U)+1, Allocator> dynMoveArray(T* arr,
size_type first, U...others) {
MDynArray<T, dims, Allocator> tmp = dynUseArray(arr,
first, other...);
tmp.ownarr = true;
return tmp;
}
};
// base class for both iterator and const_interator to ease comparisons
template <class T, int dims, class Allocator>
class MDynIteratorBase {
using itbase = typename MDynIteratorBase<T, dims, Allocator>;
public:
using size_type = typename allocator_traits<Allocator>::size_type;
using difference_type =
typename allocator_traits<Allocator>::difference_type;
protected:
MSubArray<T, dims, Allocator> elt;
size_type sz;
MDynIteratorBase(T* arr, size_type *sizes, size_type rowsize) :
elt(arr, sizes, rowsize), sz(sizes[0] * rowsize) {}
public:
bool operator ==(const itbase& other) const {
return (elt.arr == other.elt.arr) && (elt.sizes == other.elt.sizes)
&& (elt.rowsize == other.elt.rowsize);
}
bool operator != (const itbase& other) const {
return !operator ==(other);
}
bool operator <(const itbase& other) const {
return elt.arr < other.elt.arr;
}
bool operator >(const itbase& other) const {
return elt.arr > other.elt.arr;
}
bool operator <=(const itbase& other) const {
return !operator >(other);
}
bool operator >=(const itbase& other) const {
return !operator <(other);
}
protected:
itbase& add(difference_type i) { // implemented once in the base class
elt.arr += i * sz;
return *this;
}
};
// iterator if cnst == 0 or const_iterator if cnst == 1, U is the value_type
template <class T, int dims, int cnst, class Allocator, class U>
class MDynIterator: public MDynIteratorBase<T, dims, Allocator> {
using base = typename MDynIteratorBase<T, dims, Allocator>;
using base::elt;
using iterator = typename MDynIterator<T, dims, cnst, Allocator, U>;
using difference_type = typename base::difference_type;
using value_type = typename U;
using pointer = typename U*;
using reference = typename U&;
using iterator_category = std::random_access_iterator_tag;
MDynIterator(T* arr, size_type *sizes, size_type rowsize) :
base(arr, sizes, rowsize) {}
public:
// a default ctor (to mimic standard iterators)
MDynIterator(): base(nullptr, nullptr, 0) {}
//convert an (non const) iterator to a const_iterator
template <class X = T, typename = std::enable_if<cnst == 1>::type>
MDynIterator(MDynIterator<T, dims, 1 - cnst, Allocator>& other)
: base(other) {}
// all operations of an iterator
reference operator * () {
return elt;
}
pointer operator -> () {
return &elt;
}
const reference operator * () const {
return elt;
}
const pointer operator -> () const {
return &elt;
}
iterator& operator ++() {
this->add(1);
return *this;
}
iterator& operator --() {
this->add(-1);
return *this;
}
iterator operator ++(int) {
iterator tmp = *this;
this->add(1);
return tmp;
}
iterator operator --(int) {
iterator tmp = *this;
this->add(-1);
return tmp;
}
iterator& operator += (difference_type i) {
this->add(i);
return *this;
}
iterator operator + (difference_type i) {
iterator tmp = *this;
tmp.add(i);
return tmp;
}
iterator operator -= (difference_type i) {
return operator += (-i);
}
iterator operator - (difference_type i) {
return operator + (-i);
}
value_type operator [] (difference_type i) {
return *(*this + i);
}
const value_type operator [] (difference_type i) const {
return *(*this + i);
}
friend class MSubArray<T, dims+1, Allocator>;
};
}
Questions:
I tried hard to remain as close as I could to standard components (containers and iterators) but may have followed a wrong path somewhere and would really like to know where
I tried to follow modern C++ patterns but coming from good old C I may have fallen in some anti pattern and would really like to know
I would anyway be interested by any improvement
Disclaimer:
Reverse iterators are (still) not implemented. I may implement them in a later version but the question is a priori not about that point.
After Incomputable's comment, I have realized that MDynIterator did not fullfill the requirements for a bidirectional iterator because it was a stashing iterator. I do not want to change the code immediately but it will be declared as a simple forward iterator. But that also means that std::reverse_iterator
adaptor is not usable here...
Unit tests
After Toby Speight'comment, I have realized that test could be helpful here. Here is the current tests using MSVC 2017 test framework
#include "stdafx.h"
#include "CppUnitTest.h"
#include "../mdynarray/mdynarray.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace UnitTest1
{
using namespace DynArray;
TEST_CLASS(UnitTest1)
{
public:
TEST_METHOD(useArr)
{
ArrayBuilder<int> builder;
int arr[60];
int l = 0;
for (int& i : arr) {
i = l++;
}
auto dynarray = builder.dynUseArray(arr, 3, 4, 5);
l = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
for (int k = 0; k < 5; k++) {
Assert::AreSame(dynarray[i][j][k], arr[l++]);
}
}
}
}
TEST_METHOD(copyArr)
{
ArrayBuilder<int> builder;
int arr[60];
int l = 0;
for (int& i : arr) {
i = l++;
}
auto dynarray = builder.dynCopyArray(arr, 3, 4, 5);
l = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
for (int k = 0; k < 5; k++) {
Assert::AreEqual(dynarray[i][j][k], arr[l]);
Assert::AreNotSame(dynarray[i][j][k], arr[l]);
l++;
}
}
}
}
TEST_METHOD(buildArr)
{
ArrayBuilder<int> builder;
auto dynarray = builder.dynBuildArray(3, 4, 5);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
for (int k = 0; k < 5; k++) {
Assert::AreEqual(dynarray[i][j][k], 0);
}
}
}
}
TEST_METHOD(copyCtor)
{
ArrayBuilder<int> builder;
int arr[60];
int l = 0;
for (int& i : arr) {
i = l++;
}
auto dynarray = builder.dynUseArray(arr, 3, 4, 5);
auto dyn2 = dynarray;
l = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
for (int k = 0; k < 5; k++) {
Assert::AreEqual(dyn2[i][j][k], arr[l]);
Assert::AreNotSame(dyn2[i][j][k], arr[l]);
l++;
}
}
}
}
TEST_METHOD(moveCtor)
{
ArrayBuilder<int> builder;
int arr[60];
int l = 0;
for (int& i : arr) {
i = l++;
}
auto dynarray = builder.dynUseArray(arr, 3, 4, 5);
auto dyn2 = std::move(dynarray);
Assert::AreEqual(dynarray.size(), 0u);
l = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
for (int k = 0; k < 5; k++) {
Assert::AreSame(dyn2[i][j][k], arr[l]);
l++;
}
}
}
}
TEST_METHOD(copyAssign)
{
ArrayBuilder<int> builder;
int arr[60];
int l = 0;
for (int& i : arr) {
i = l++;
}
auto dynarray = builder.dynUseArray(arr, 3, 4, 5);
auto dyn2 = builder.dynBuildArray(3, 4, 5);
dyn2 = dynarray;
l = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
for (int k = 0; k < 5; k++) {
Assert::AreEqual(dyn2[i][j][k], arr[l]);
Assert::AreNotSame(dyn2[i][j][k], arr[l]);
l++;
}
}
}
}
TEST_METHOD(moveAssign)
{
ArrayBuilder<int> builder;
int arr[60];
int l = 0;
for (int& i : arr) {
i = l++;
}
auto dynarray = builder.dynUseArray(arr, 3, 4, 5);
auto dyn2 = builder.dynBuildArray(3, 4, 5);
dyn2 = std::move(dynarray);
Assert::AreEqual(dynarray[1][1][1], 0); // Beware implementation test
l = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
for (int k = 0; k < 5; k++) {
Assert::AreSame(dyn2[i][j][k], arr[l]);
l++;
}
}
}
}
TEST_METHOD(nonConstIter)
{
ArrayBuilder<int> builder;
int arr[60];
int l = 0;
for (int& i : arr) {
i = l++;
}
auto dynarray = builder.dynUseArray(arr, 3, 4, 5);
l = 0;
for (auto& it1 : dynarray) {
for (auto& it2 : it1) {
for (auto& it3 : it2) {
Assert::AreSame(it3, arr[l]);
l++;
it3 = l; // control it is not const...
}
}
}
}
TEST_METHOD(constIter)
{
ArrayBuilder<int> builder;
int arr[60];
int l = 0;
for (int& i : arr) {
i = l++;
}
auto dynarray = builder.dynUseArray(arr, 3, 4, 5);
l = 0;
for (auto it1 = dynarray.cbegin(); it1 != dynarray.cend(); it1++) {
for (auto it2 = it1->cbegin(); it2 != it1->cend(); it2++) {
for (auto it3 = it2->cbegin(); it3 != it2->cend(); it3++) {
Assert::AreSame(*it3, arr[l]);
l++;
// *it3 = l; // does not compile
}
}
}
}
TEST_METHOD(convConstIterator)
{
ArrayBuilder<int> builder;
int arr[60];
int l = 0;
for (int& i : arr) {
i = l++;
}
auto dynarray = builder.dynUseArray(arr, 3, 4, 5);
auto it = dynarray.begin();
MDynArray<int, 3>::const_iterator cit = it;
//it = (MDynArray<int, 3>::iterator) cit; // does not compile
it += 1;
cit += 1;
Assert::IsTrue(it > dynarray.begin());
Assert::IsTrue(it == cit);
Assert::IsTrue(cit == it);
}
TEST_METHOD(revIterator)
{
ArrayBuilder<int> builder;
int arr[60];
int l = 0;
for (int& i : arr) {
i = l++;
}
auto dynarray = builder.dynUseArray(arr, 3, 4, 5);
l = 0;
for (auto it1 = dynarray.rbegin(); it1 != dynarray.rend(); it1++) {
for (auto it2 = it1->rbegin(); it2 != it1->rend(); it2++) {
for (auto it3 = it2->rbegin(); it3 != it2->rend(); it3++) {
Assert::AreSame(*it3, arr[59 - l]);
l++;
*it3 = l; // control non constness
}
}
}
}
};
}
std::reverse_iterator
didn't suit you? \$\endgroup\$std::reverse_iterator
later but wanted to get advices on the existing code. I think that usingstd::reverse_iterator
should be almost straigthforward (except maybe the comparisons functions) and prefered not to spend to much time on it immediately. And also thought that current code was already long enough... \$\endgroup\$std::reverse_iterator
and it immediately broke in my test. I realized thatMDynIterator
was a stashing iterator (returns pointer or reference to a member object) and because of it did not fullfill the requirements of a bidirectional iterator. So your comment was as helpful as a review... \$\endgroup\$