I would recommend using a one-dimensional array, as user2079303 suggested in their answer. If this isn't desirable, however, it's also possible to implement your idea with either templates or polymorphism.
If you don't need to be able to change its dimensions on the fly, you could make your matrix a templated class, using its dimensions as template parameters. This, in turn, allows the dimensions to be used for compile-time logic with SFINAE, which allows you to overload operator[]()
based on the matrix's dimensions.
Note that this doesn't actually create a 1D array when Rows == 1
or Cols == 1
, it merely simulates one; if you explicitly need that behaviour, instead of technically different but mostly equivalent behaviour, you'll want to look into specialising the class for when one or both parameters is 1
. This will likely involve duplicating at least some of the code, so it'll be a bit messy.
// This example uses std::array for the actual array, since it's compile-time anyways.
// This, in turn, lets it declare a lot of functions constexpr.
// Const-correctness omitted for brevity. Remember to restore it for actual code.
template<size_t Rows, size_t Cols>
class Matrix {
std::array<std::array<double, Cols>, Rows> mat;
public:
// Default constructor. Clang _really_ likes braced initialiser lists.
constexpr Matrix() : mat{{{{0}}}} {}
// Array constructor.
constexpr Matrix(const decltype(mat)& arr) : mat(arr) {}
// -----
// Subscript operators.
// Generic operator. Matrix<x, y>, where x != 1 && y != 1.
// Does what normal subscript operators do.
template<bool R = (Rows == 1), bool C = (Cols == 1)>
auto& operator[](std::enable_if_t<!R && !C, size_t> i) {
return mat[i];
}
// Magic operator. Matrix<1, x>, where x != 1.
template<bool R = (Rows == 1), bool C = (Cols == 1)>
auto& operator[](std::enable_if_t<(R && !C), size_t> i) {
return mat[0][i];
}
// Magic operator. Matrix<x, 1>, where x != 1.
template<bool R = (Rows == 1), bool C = (Cols == 1)>
auto& operator[](std::enable_if_t<C && !R, size_t> i) {
return mat[i][0];
}
// Scalar matrix operator. Matrix<1, 1>.
// Just returns mat[0][0], for simplicity's sake. Might want to make it do something
// more complex in your actual class.
template<bool R = (Rows == 1), bool C = (Cols == 1)>
auto& operator[](std::enable_if_t<R && C, size_t> i) {
return mat[0][0];
}
// -----
// A few interface helpers.
// Simple begin() & end(), for example's sake.
// A better version would begin at mat[0][0] and end at mat[Rows - 1][Cols - 1].
constexpr auto begin() const { return mat.begin(); }
constexpr auto end() const { return mat.end(); }
// Generic helpers.
constexpr size_t size() const { return mat.size() * mat[0].size(); }
constexpr size_t rows() const { return mat.size(); }
constexpr size_t cols() const { return mat[0].size(); }
// 1D Matrix helpers.
constexpr bool is_one_d() const { return (Rows == 1) || (Cols == 1); }
constexpr bool one_row() const { return Rows == 1; }
constexpr size_t dimension() const { return (one_row() ? cols() : rows()); }
// -----
// Output.
// Would need modification if better begin() & end() are implemented.
friend std::ostream& operator<<(std::ostream& str, const Matrix<Rows, Cols>& m) {
for (auto& row : m) {
for (auto& elem : row) {
str << std::setw(6) << elem << ' ';
}
str << '\n';
}
str << std::endl;
return str;
}
};
It can be used as...
// Get rid of any "Waah, you didn't use that!" warnings.
// See https://stackoverflow.com/a/31654792/5386374
#define UNUSED(x) [&x]{}()
// This should really use if constexpr, but online compilers don't really support it yet.
// Instead, have an SFINAE dummy.
template<size_t Rows, size_t Cols>
void fill(Matrix<Rows, Cols>& m, std::enable_if_t<(Rows == 1) || (Cols == 1), int> dummy = 0) {
UNUSED(dummy);
//for (size_t i = 0; i < (m.one_row() ? m.cols() : m.rows()); i++) {
for (size_t i = 0; i < m.dimension(); i++) {
m[i] = (i ? i : 0.5) * (i ? i : 0.5);
}
}
template<size_t Rows, size_t Cols>
void fill(Matrix<Rows, Cols>& m, std::enable_if_t<!((Rows == 1) || (Cols == 1)), int> dummy = 0) {
UNUSED(dummy);
for (size_t i = 0; i < m.rows(); i++) {
for (size_t j = 0; j < m.cols(); j++) {
m[i][j] = (i ? i : 0.5) * (j ? j : 0.5) + (i >= j ? 0.1 : -0.2);
}
}
}
See it in action here.
If you do need to be able to change its dimensions on the fly, this becomes more complex. The easiest solution would probably be to use polymorphism, and have operator[]
return a proxy.
class Matrix {
protected:
// Out proxy class.
// Visible to children, for implementing.
struct SubscriptProxy {
virtual operator double&() = 0;
virtual operator double*() = 0;
virtual double& operator=(double) = 0;
virtual double& operator[](size_t) = 0;
virtual ~SubscriptProxy() = default;
};
public:
virtual SubscriptProxy& operator[](size_t i) = 0;
virtual ~Matrix() = default;
virtual void out(std::ostream& str) const = 0;
friend std::ostream& operator<<(std::ostream& str, const Matrix& m) {
m.out(str);
return str;
}
};
std::ostream& operator<<(std::ostream& str, const Matrix& m);
You can then have each class make the proxy members it needs public
, and the ones it doesn't private
; the private
ones get a non-functional dummy implementation.
// Resizing omitted for brevity.
class OneDMatrix : public Matrix {
double arr[5];
// Proxy for single element.
class OneDProxy : public SubscriptProxy {
double& elem;
operator double*() override { return &elem; }
double& operator[](size_t) override { return elem; }
public:
OneDProxy(double& e) : elem(e) {}
operator double&() override { return elem; }
double& operator=(double d) override {
elem = d;
return elem;
}
};
public:
OneDMatrix() : arr{0} {}
// operator[] maintains a static pointer, to keep the return value alive and guarantee
// proper cleanup.
SubscriptProxy& operator[](size_t i) override {
static OneDProxy* ret = nullptr;
if (ret) { delete ret; }
ret = new OneDProxy(arr[i]);
return *ret;
}
void out(std::ostream& str) const override {
for (size_t i = 0; i < 5; i++) {
str << std::setw(4) << arr[i] << ' ';
}
str << std::endl;
}
};
// Resizing omitted for brevity.
class TwoDMatrix : public Matrix {
double arr[3][4];
// Proxy for array.
class TwoDProxy : public SubscriptProxy {
double* elem;
operator double&() override { return elem[0]; }
double& operator=(double) override { return elem[0]; }
public:
TwoDProxy(double* e) : elem(e) {}
operator double*() override { return elem; }
double& operator[](size_t i) override { return elem[i]; }
};
public:
TwoDMatrix() : arr{{0}} {}
// operator[] maintains a static pointer, to keep the return value alive and guarantee
// proper cleanup.
SubscriptProxy& operator[](size_t i) override {
static TwoDProxy* ret = nullptr;
if (ret) { delete ret; }
ret = new TwoDProxy(arr[i]);
return *ret;
}
void out(std::ostream& str) const override {
for (size_t i = 0; i < 3; i++) {
for (size_t j = 0; j < 4; j++) {
str << std::setw(4) << arr[i][j] << ' ';
}
str << '\n';
}
}
};
Due to references being usable for polymorphism, Matrix
can be used as an interface, allowing anything that doesn't specifically require one of the actual classes to take a Matrix&
.
See it in action here.
With a setup like this, it becomes easy to dynamically create the right kind of matrix, by providing a helper function.
// Assume OneDMatrix is expanded for dynamic size specification.
// It now has constructor OneDMatrix(size_t sz), and owns a dynamic double[sz].
// Assume TwoDMatrix is expanded for dynamic size specification.
// It now has constructor TwoDMatrix(size_t r, size_t c), and owns a dynamic double[r][c].
Matrix* createMatrix(size_t rows, size_t cols) {
if (rows == 1) { return new OneDMatrix(cols); }
else if (cols == 1) { return new OneDMatrix(rows); }
else { return new TwoDMatrix(rows, cols); }
}
As usual, the template version is more verbose, but safer (due to not having to play around with pointers) and likely more efficient (due to not needing to do as much dynamic allocation & deallocation; I haven't tested this, however).