'Can one make a 2D matrix class where often accessed sizes, rows and cols, are const members and not getter functions?

I'm continually irritated that matrix dimensions are accessed with getters. Sure one can have public rows and cols but these should be const so that users of the Matrix class can't directly modify them. This is a natural way of coding but, because using const members is regarded, apparently incorrectly, as not possible without UB, results in people using non-const fields when they really should be consts that are mutable when needed such as resizing, or assigning.

What I'd like is something that can be used like this:

Matrix2D<double> a(2, 3);
int index{};
for (int i = 0; i < a.rows; i++)
    for (int ii = 0; ii < a.cols; ii++)
        a(i, ii) = index++;

but where a.rows=5; isn't compilable because it's const. And it would be great if the class can be included in vectors and other containers.

Now comes: Implement C++20's P0784 (More constexpr containers) https://reviews.llvm.org/D68364?id=222943

It should be doable without casts or UB and can even be done when evaluating const expressions. I believe c++20 has made it doable by providing the functions std::destroy_at and std::construct_at. I have attached an answer using c++20 that appears to show it is indeed possible, and easily done, to provide const member objects in classes without an undue burden.

So the question is do the new constexpr functions in c++20, which provide the ability to destroy and then construct an object with different consts valid? It certainly appears so and it passes the consexpr lack of UB test.

c++


Solution 1:[1]

If you want to have const properties you can try this :

class Matrix {
    public:
        const int rows;
        const int cols;
        
        Matrix(int _rows, int _cols) : rows(_rows), cols(_cols)
        {
            
        }
};

The rows and cols properties of a Matrix instance are initialized once in the constructor.

Solution 2:[2]

Well, here's my pass at using const rows and cols in a 2D matrix. It passes UB tests and can be used in collections like vector w/o weird restrictions. It's a partial implementation. No bounds checking and such but to provide a possible approach to the problem.

matrix2d.h

#pragma once
#include <memory>
#include <vector>
#include <initializer_list>
#include <stdexcept>
#include <array>

template<class T>
class Matrix2D
{
    std::vector<T> v;
public:
    const size_t rows;
    const size_t cols;
    constexpr Matrix2D(const Matrix2D& m) = default;
    constexpr Matrix2D(Matrix2D&& m) noexcept = default;
    explicit constexpr Matrix2D(size_t a_rows=0, size_t a_cols=0) : v(a_rows* a_cols), rows(a_rows), cols(a_cols) {}
    constexpr Matrix2D(std::initializer_list<std::initializer_list<T>> list) : rows(list.size()), cols((list.begin())->size())
    {
        for (auto& p1 : list)
            for (auto p2 : p1)
                v.push_back(p2);
    }
    constexpr Matrix2D& operator=(const Matrix2D& mat)
    {
        std::vector<T> tmp_v = mat.v;
        std::construct_at(&this->rows, mat.rows);
        std::construct_at(&this->cols, mat.cols);
        this->v.swap(tmp_v);
        return *this;
    }
    constexpr Matrix2D& operator=(Matrix2D&& mat) noexcept
    {
        std::construct_at(&this->rows, mat.rows);
        std::construct_at(&this->cols, mat.cols);
        this->v.swap(mat.v);
        return *this;
    }

    // user methods
    constexpr T* operator[](size_t row) {   // alternate bracket indexing
        return &v[row * cols];
    };
    constexpr T& operator()(size_t row, size_t col)
    {
        return v[row * cols + col];
    }
    constexpr const T& operator()(size_t row, size_t col) const
    {
        return v[row * cols + col];
    }
    constexpr Matrix2D operator+(Matrix2D const& v1) const
    {
        if (rows != v1.rows || cols != v1.cols) throw std::range_error("cols and rows must be the same");
        Matrix2D ret = *this;
        for (size_t i = 0; i < ret.v.size(); i++)
            ret.v[i] += v1.v[i];
        ret.v[0] = 3;
        return ret;
    }
    constexpr Matrix2D operator-(Matrix2D const& v1) const
    {
        if (rows != v1.rows || cols != v1.cols) throw std::range_error("cols and rows must be the same");
        Matrix2D ret = *this;
        for (size_t i = 0; i < ret.v.size(); i++)
            ret.v[i] -= v1.v[i];
        return ret;
    }
    constexpr Matrix2D operator*(Matrix2D const& v1) const
    {
        if (cols != v1.rows) throw std::range_error("cols of first must == rows of second");
        Matrix2D v2 = v1.transpose();
        Matrix2D ret(rows, v1.cols);
        for (size_t row = 0; row < rows; row++)
            for (size_t col = 0; col < v1.cols; col++)
            {
                T tmp{};
                for (size_t row_col = 0; row_col < cols; row_col++)
                    tmp += this->operator()(row, row_col) * v2(col, row_col);
                ret(row, col) = tmp;
            }
        return ret;
    }
    constexpr Matrix2D transpose() const
    {
        Matrix2D ret(cols, rows);
        for (size_t r = 0; r < rows; r++)
            for (size_t c = 0; c < cols; c++)
                ret(c, r) = this->operator()(r, c);
        return ret;
    }

    // Adds rows to end
    constexpr Matrix2D add_rows(const Matrix2D& new_rows)
    {
        if (cols != new_rows.cols) throw std::range_error("cols cols must match");
        Matrix2D ret = *this;
        std::construct_at(&ret.rows, rows + new_rows.rows);
        ret.v.insert(ret.v.end(), new_rows.v.begin(), new_rows.v.end());
        return ret;
    }

    constexpr Matrix2D add_cols(const Matrix2D& new_cols)
    {
        if (rows != new_cols.rows) throw std::range_error("rows must match");
        Matrix2D ret(rows, cols + new_cols.cols);
        for (size_t row = 0; row < rows; row++)
        {
            for (size_t col = 0; col < cols; col++)
                ret(row, col) = this->operator()(row, col);
            for (size_t col = cols; col < ret.cols; col++)
                ret(row, col) = new_cols(row, col - cols);
        }
        return ret;
    }

    constexpr bool operator==(Matrix2D const& v1) const
    {
        if (rows != v1.rows || cols != v1.cols || v.size() != v1.v.size())
            return false;
        for (size_t i = 0; i < v.size(); i++)
            if (v[i] != v1.v[i])
                return false;
        return true;
    }
    constexpr bool operator!=(Matrix2D const& v1) const
    {
        return !(*this == v1);
    }

    // friends
    template <size_t a_rows, size_t a_cols>
    friend constexpr auto make_std_array(const Matrix2D& v)
    {
        if (a_rows != v.rows || a_cols != v.cols) throw std::range_error("template cols and rows must be the same");
        std::array<std::array<T, a_cols>, a_rows> ret{};
        for (size_t r = 0; r < a_rows; r++)
            for (size_t c = 0; c < a_cols; c++)
                ret[r][c] = v(r, c);
        return ret;
    }
};

Source.cpp

#include <vector>
#include <algorithm>
#include <iostream>
#include "matrix2d.h"

constexpr std::array<std::array<int, 3>, 4> foo()
{
    Matrix2D<double> a(2, 3), b;
    Matrix2D d(a);
    int index{};
    for (int i = 0; i < a.rows; i++)
        for (int ii = 0; ii < a.cols; ii++)
            a(i, ii) = index++;
    b = a + a;

    Matrix2D<int> z1{ {1},{3},{5} };
    z1 = z1.add_cols({{2}, {4}, {6}});
    z1 = z1.add_rows({{7,8}});
    // z1 is now {{1,2},{3,4},{5,6},{7,8}}
    Matrix2D<int> z2{ {1,2,3},{4,5,6} };
    Matrix2D<int> z3 = z1 * z2;     // z3: 4 rows, 3 cols

    // from separate math program product of z1 and z2
    Matrix2D<int> ref{ {9,12,15},{19,26,33},{29,40,51},{39,54,69} };

    // test transpose
    Matrix2D<int> tmp = ref.transpose(); // tmp: now 3 rows, 4 cols
    if (ref == tmp) throw;  // verify not equal
    tmp = tmp.transpose();  // now ref==tmp==z3
    if (ref != tmp || ref != z3) throw;

    // Check using vector of matrixes, verify sort on row size works
    std::vector<Matrix2D<int>> v;
    v.push_back(z1);
    v.push_back(z2);
    v.push_back(z1);
    v.push_back(z2);
    std::sort(v.begin(), v.end(), [](auto const& m1, auto const& m2) {return m1.rows < m2.rows; });
    for (size_t i = 0; i < v.size() - 1; i++)
        if (v[i].rows > v[i + 1].rows) throw;

    // return constexpr of 2D std::array
    std::array<std::array<int, 3>, 4> array = make_std_array<4,3>(ref); // ref must have 4 rows, 3 cols
    return array;
}


int main()
{
    auto print = [](auto& a) {
        for (auto& x : a)
        {
            for (auto y : x)
                std::cout << y << " ";
            std::cout << '\n';
        }
        std::cout << '\n';
    };

    // run time
    auto zz = foo();
    print(zz);

    // validating no UB in Matrix2D;
    // and creating a nested, std::array initialized at compile time
    //constexpr std::array<std::array<int, 3>, 4>  x = foo();
    constexpr std::array<std::array<int, 3>, 4>  x = foo(); // compile time
    print(x);
    return 0;
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2