The other answer shows a good way of making a vector of vectors with template metaprogramming. If you want a multidimensional array data structure with fewer allocations and contiguous storage underneath, here is an example of how to achieve that with an NDArray template class wrapping access to an underlying vector. This could be extended to define non-default operator=, copy operators, debug bounds checking per dimension, row-major or column-major storage, etc for extra convenience.
NDArray.h
#pragma once
#include <array>
#include <vector>
template<int N, typename ValueType>
class NDArray {
public:
template<typename... Args>
NDArray(Args... args)
: dims({ args... }),
offsets(compute_offsets(dims)),
data(compute_size(dims), ValueType{})
{
static_assert(sizeof...(args) == N,
"Incorrect number of NDArray dimension arguments");
}
void fill(ValueType val) {
std::fill(data.begin(), data.end(), val);
}
template<typename... Args>
inline void resize(Args... args) {
static_assert(sizeof...(args) == N,
"Incorrect number of NDArray resize arguments");
dims = { args... };
offsets = compute_offsets(dims);
data.resize(compute_size(dims));
fill(ValueType{});
}
template<typename... Args>
inline ValueType operator()(Args... args) const {
static_assert(sizeof...(args) == N,
"Incorrect number of NDArray index arguments");
return data[calc_index({ args... })];
}
template<typename... Args>
inline ValueType& operator()(Args... args) {
static_assert(sizeof...(args) == N,
"Incorrect number of NDArray index arguments");
return data[calc_index({ args... })];
}
int length(int axis) const { return dims[axis]; }
const int num_dims = N;
private:
static std::array<int, N> compute_offsets(const std::array<int, N>& dims) {
std::array<int, N> offsets{};
offsets[0] = 1;
for (int i = 1; i < N; ++i) {
offsets[i] = offsets[i - 1] * dims[i - 1];
}
return offsets;
}
static int compute_size(const std::array<int, N>& dims) {
int size = 1;
for (auto&& d : dims) size *= d;
return size;
}
inline int calc_index(const std::array<int, N>& indices) const {
int idx = 0;
for (int i = 0; i < N; ++i) idx += offsets[i] * indices[i];
return idx;
}
std::array<int, N> dims;
std::array<int, N> offsets;
std::vector<ValueType> data;
};
This overrides the operator() with the correct number of arguments, and won't compile if the wrong number of arguments is given. Some example use
using Array2D = NDArray<2,double>;
using Array3D = NDArray<3,double>;
auto a = Array2D(3, 6);
a.fill(1.0);
a(2, 4) = 2.0;
//a(2,4,4) will not compile
std::cout << "a = " << std::endl << a << std::endl;
a.resize(2,2);
a(1,1) = 1.2;
std::cout << "a = " << std::endl << a << std::endl;
//auto b = Array3D(4, 4); // will not compile
auto b = Array3D(4, 3, 2);
b.fill(-1.0);
b(0, 0, 0) = 4.0;
b(1, 1, 1) = 2.0;
std::cout << "b = " << std::endl << b << std::endl;
(using helper output methods for 2D and 3D arrays)
std::ostream& operator<<(std::ostream& os, const Array2D& arr) {
for (int i = 0; i < arr.length(0); ++i) {
for (int j = 0; j < arr.length(1); ++j) {
os << arr(i,j) << " ";
}
os << std::endl;
}
return os;
}
std::ostream& operator<<(std::ostream& os, const Array3D& arr) {
for (int k = 0; k < arr.length(2); ++k) {
os << "array(:,:,"<<k<<") = " << std::endl;
for (int i = 0; i < arr.length(0); ++i) {
os << " ";
for (int j = 0; j < arr.length(1); ++j) {
os << arr(i, j, k) << " ";
}
os << std::endl;
}
os << std::endl;
}
return os;
}
vec[i*rows+j]?[]operator)?operator()with N arguments to handle the indexing easily enough.