This answer differs in two points from most of the other answers:
- no raw pointer or memory allocation
- use a single contiguous (linear) block of memory
A common approach is using a single linear block of memory and storing the elements row after row or column after column (Row- or column-major order). Let us generalize a bit and say you want a 2D quantity of type T
with the extents N0
and N1
. Then you can obtain the required memory by
std::vector<T> my_memory(N0*N1, T{});
Using a vector instead of a raw pointer has several advantages. For example, you can not forget to free the memory because the deconstructor cares about that automatically (RAII idiom). Another advantage is that the constructor called above fills the vector with copies of default-constructed T
(instead of leaving the memory uninitialized).
Next, you can access the linear memory by using vector
's member operator[]
(or the member function at
if you want bound checks). You probably want to use two indices n0
and n1
in the intervals [0
, N0
) and [0
, N1
), but the memory is linear and requires a single index. Hence you can introduce a helper function index
in order to access your 2D quantity as, say,
my_memory[index(n0, n1)] = T(42);
To imagine how this could work consider a 3-by-2 matrix. In the given linear memory block you could arrange the elements like this:
0 1 2 3 4 5
index(0, 0) index(0, 1) index(1, 0) index(1, 1) index(2, 0) index(2, 1)
In which case you would write
int index(int n0, int n1) {
return n0 * N1 + n1;// (note that `n0` is multiplied by `N1`).
}
In general for d
dimensions you would have (pseudo code)
int index(int(&n)[d]) {
return n[0] * (N[1]*N[2]*...*N[d-1])
+ n[1] * (N[2]*N[3]*...*N[d-1])
+ ...
+ n[d-2] * (N[d-1])
+ n[d-1] * (1);
}
where the expressions in brackets are also called pitches.
If you put all that logic in a class, is becomes very simple to use. Here is an example for inspiration (online demo with N = 4
):
#include <cassert>
#include <iostream>
#include <vector>
template<class T>
class Quantity2d {
private:
std::size_t Ns_[2];
std::vector<T> data_;
public:
Quantity2d(int N0, int N1)
: Ns_{std::size_t(N0), std::size_t(N1)}
, data_()
{
assert(N0 > 0);
assert(N1 > 0);
data_.resize(size(), T{});
}
constexpr size_t size() const { return Ns_[0] * Ns_[1]; }
constexpr int N0() const { return int(Ns_[0]); }
constexpr int N1() const { return int(Ns_[1]); }
constexpr size_t pitch0() const { return Ns_[1]; }
constexpr size_t pitch1() const { return std::size_t(1); }
constexpr size_t ind(int n0, int n1) const {
assert(n0 >= 0 && n0 < N0());
assert(n1 >= 0 && n1 < N1());
return std::size_t(n0) * pitch0() + std::size_t(n1) * pitch1();
}
T& operator()(int n1, int n2) {
return data_[ind(n1, n2)];
}
constexpr const T& operator()(int n1, int n2) const {
return data_[ind(n1, n2)];
}
};
template<class T>
std::ostream& operator<<(std::ostream& os, const Quantity2d<T>& q) {
int N0 = q.N0();
int N1 = q.N1();
for(int i=0; i<N0; ++i) {
for(int j=0; j<N1; ++j) {
os << i << "\t" << j << "\t" << q(i, j) << "\n";
}
}
return os;
}
int main() {
int N = 0;
std::cout << "please enter size per dimension "
<< "(WARNING: matrix will be printed on screen): "
<< std::flush;
std::cin >> N;
if(N >= 1) {
std::cout << "I understood. So let's do " << N << "x" << N << std::endl;
}
else {
std::cerr << "You failed. Next time try with N >= 1." << std::endl;
return 1;
}
Quantity2d<double> matrix(N, N);
for(int i=0; i<matrix.N0(); ++i) {
for(int j=0; j<matrix.N1(); ++j) {
matrix(i, j) = double(i) * double(j);
}
}
std::cout << matrix << std::flush;
return 0;
}