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_iteratordidn't suit you? \$\endgroup\$std::reverse_iteratorlater but wanted to get advices on the existing code. I think that usingstd::reverse_iteratorshould 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_iteratorand it immediately broke in my test. I realized thatMDynIteratorwas 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\$