-1

I am currently implementing a custom matrix class in C++ inspired by the functionality of Numpy. My goal is to enable indexing behavior where 1-dimensional matrices can be accessed using a single bracket notation, and 2-dimensional matrices can be accessed using two brackets, similar to the example provided in the main function of my code.

Here is my current code:

#include <iostream>
#include <cmath>
#include <vector>

namespace np {
    template <typename T>
    class matrix {
        private:
            std::vector<std::vector<T>> mat;
            int rows;
            int cols;

        public:
            matrix(int numRows, int numCols) : rows(numRows), cols(numCols) {
                mat.resize(rows, std::vector<T> (cols, T()));
            }

            matrix(std::initializer_list<T> initlist) : mat({initlist}), rows(1), cols(initlist.size()) {}

            matrix(std::initializer_list<std::initializer_list<T>> initlist) : rows(initlist.size()), cols(initlist.begin()->size()) {
                for (const auto& rowElement : initlist) {
                    mat.emplace_back(rowElement);
                    if (rowElement.size() != cols) {
                        throw std::runtime_error("Initializer list is not rectangular.");
                    }
                }
            }

            T& operator[](int index) {
                return mat[index];
            }

            const T& operator[](int index) const {
                return mat[index];
            }
    };        
};

int main() {
    np::matrix<int> mat(3, 3);
    np::matrix<int> mat1 = {{1, 2, 3}, {4, 5, 6}};
    np::matrix<int> mat2 = {1, 2, 3, 4, 5, 6};

    std::cout << mat1[1][2]; // should access 6;
    std::cout << mat1[4]; // should access 5 
}

Currently, the code supports two-bracket indexing, allowing me to access index [i, j]. However, I would like to extend this functionality to enable one-bracket indexing, which would allow me to access individual elements of a matrix. I am seeking guidance on how to achieve this.

I have heard that variadic templates and recursive calls can be used to handle multiple levels of indexing. How can I modify my code to incorporate these techniques and achieve the desired indexing behavior?

I appreciate any assistance or suggestions you can provide. Thank you in advance!

user4581301
  • 33,082
  • 7
  • 33
  • 54
  • 1
    Side note: You're shooting yourself in the face speed-wise with `std::vector> mat;` because the data is not contiguous in memory. Consider using something like [what Doug does here instead.](https://stackoverflow.com/a/36123944/4581301) – user4581301 Jun 09 '23 at 17:54
  • Additional handy reading: [How do I create a subscript operator for a Matrix class?](https://isocpp.org/wiki/faq/operator-overloading#matrix-subscript-op) – user4581301 Jun 09 '23 at 17:58
  • "Currently, the code supports two-bracket indexing ... However, I would like to extend this functionality to enable two-bracket indexing" hm? – 463035818_is_not_an_ai Jun 09 '23 at 18:03
  • @463035818_is_not_a_number yeah I have to edit that out – Sayed Aulia Jun 09 '23 at 18:04
  • If you use the function call operator rather than trying to use the subscript operator your life will be easier. You can't change `[]` to have different return types for 1D and multi D, but can overlload `()` to take different numbers of parameters. – user4581301 Jun 09 '23 at 18:10
  • @user4581301 is there a good way to use [] still to stick with convention? – Sayed Aulia Jun 09 '23 at 18:18
  • 3
    A multidimensional subscript operator will be added in C++23. Until then, you can overload `operator()`. – Bob__ Jun 09 '23 at 18:19
  • I believe you should be able to return a proxy that knows how to convert to a `T` or to a `T*` depending on what's needed, but I'd have to play around with that a bit. – user4581301 Jun 09 '23 at 18:27
  • Here's a quick hack cut of a proxy-based solution. I don't have time to properly test it right now: https://godbolt.org/z/7qsP9bsaW As you can see, it's not particularly clean and you can't directly `<<` it. – user4581301 Jun 09 '23 at 18:54
  • Thats very neat, thank you. If you dont mind could you explain the code? – Sayed Aulia Jun 09 '23 at 18:57
  • @user4581301 You show how to do `mat[i][j]` but OP already does that (in a different way), the question is how to do `mat[i,j]`. – n. m. could be an AI Jun 09 '23 at 19:46
  • If that's the case I misread the question and Bob__'s comment is even more right than I thought. But the asker's shot at `[i][j]` wasn't going to work they way they wanted because they want to to use both `[i]` and `[i][j]` on the same matrix. Sayed, can you clarify what you are looking for? – user4581301 Jun 09 '23 at 19:50
  • @user4581301 No wait, it looks like I'm misreading something. OP wants `mat1[4]` to access 5, which doesn't make any sense to me. – n. m. could be an AI Jun 09 '23 at 20:06
  • According to [cppreference](https://en.cppreference.com/w/cpp/compiler_support) "Multidimensional subscript operator" is supported in gcc 12 (13 was released recently) and clang 15 (16 is also already released), so I would say that at least in the Linux world one does not have to wait to use that C++23 feature. Even Apple clang seems to support it. – paleonix Jun 09 '23 at 21:17
  • Note that C++23 also brings `std::mdspan`. If you are working on production code I would recommend using this new library feature for implementing anything matrix related. While the compilers don't seem to ship it yet, there are reference implementations available like [`kokkos/mdspan`](https://github.com/kokkos/mdspan) – paleonix Jun 09 '23 at 21:26
  • @user4581301 Indeed im trying to use both – Sayed Aulia Jun 09 '23 at 23:12

2 Answers2

0

Your requirements are self-contradictory

You want mat1[4] to access 5. It stands for reason tat mat[0] should be 1. On the other hand, mat[0] is something that you can index, e.g. mat[0][1] should be 2.

So if mat[0] is X, then on one hand X == 1 and on the other hand X[1] == 2. So 1[1] == 2. This doesn't make a lot of sense.

While it technically possible to create an object that can be seen as both a number and an indexable array of numbers depending on context, it would be a highly confusing and counter-intuitive (and error-prone) object. Don't do that. Don't make your fellow programmers decipher that. They won't thank you.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
0

operator [] handles one-and-only-one index until the C++23 Standard becomes official and C++23 compilers are widely available.

Things are further complicated by the desire for the ability to access 2D structures the same way you access 1D structures. You cannot override [] (or any other function) with a change of return type in order to return something you can invoke the inner dimension's [] on AND also return the value.

First, the smart way: Use operator(). This should easily convert to C++23's improved operator[]

template<typename T>
class matrix
{
private:
    std::vector<T> mat; // faster and more versatile as a 1D data structure.
                        // we will perform the indexing math, if necessary,
                        // on demand below. This may sound slow, extra math,
                        // but guess what [][] is doing behind the scenes
                        // anyway? That's right! Same dang thing.
                        // What we get is 1D matrixes are now dead simple
                        // and we have one contiguous block of memory and
                        // this make it dead easy to for the CPU to cache

    size_t rows;    // using size_t instead of in for larger size and who
    size_t cols;    // needs a negative array size?

public:

    // just regular old constructors pretty much unchanged from the asker's 
    // originals other than flattening out the nested vectors
    matrix(int numRows, int numCols) :
            mat(numRows * numCols), rows(numRows), cols(numCols)
    {
    }

    matrix(std::initializer_list<T> initlist) :
            mat( { initlist }), rows(1), cols(initlist.size())
    {
    }

    matrix(std::initializer_list<std::initializer_list<T>> initlist) :
            rows(initlist.size()), cols(initlist.begin()->size())
    {
        for (const auto &rowElement : initlist)
        {
            mat.insert(mat.end(), rowElement.begin(), rowElement.end());
            if (rowElement.size() != cols)
            {
                throw std::runtime_error("Initializer list is not rectangular.");
            }
        }
    }

    // uses operator ()
    T& operator()(size_t index)
    {
        return mat[index];
    }

    T& operator()(size_t row, size_t col)
    {
        return mat[row * cols + col]; // flatten 2D indexing into 1D
    }

    T operator()(size_t index) const
    {
        return mat[index];
    }

    T operator()(size_t row, size_t col) const
    {
        return mat[row * cols + col];
    }
};

Usage:

np::matrix<int> mat(3, 3);
np::matrix<int> mat1 = { { 1, 2, 3 }, { 4, 5, 6 } };
np::matrix<int> mat2 = { 1, 2, 3, 4, 5, 6 };
const np::matrix<int> mat3 = mat1; // test for const-correctness

std::cout << mat1(1,2) << std::endl;
std::cout << mat1(4) << std::endl;
std::cout << mat3(1,2) << std::endl;
std::cout << mat3(4) << std::endl;

Now here's the simplest way I can come up with to do it with the pre C++23 operator[] and preserve the [i][j] syntax.

template<typename T>
class matrix
{
private:
    std::vector<T> mat; 

    size_t rows;    
    size_t cols;    

public:

    // you cannot pass more than one index to operator [] (at least not
    // until C++23). But what you can do is have [] return a proxy object
    // that allows access to the inner dimension
    template <class U>
    class proxy
    {
        matrix &ref;
        size_t index;

        // converts the proxy to a T (int in this case), but can be called
        // on a constant proxy. Needed for operator << because operator T&()
        // cannot perform T x = val; when val is const
        operator T() const
        {
            return ref.mat[index];
        }
    public:
        // just constructs the proxy around the around the matrix and holds
        // onto the index used so we have it later.
        proxy(matrix &ref,
              size_t index): ref(ref), index(index)
        {

        }
        // converts the proxy to a T (int in this case)
        operator T&()
        {
            return ref.mat[index]; // by reading the T out of the matrix directly
        }
        // accesses the proxy as an array so that we can get the value at
        // the inner dimension
        T& operator [](size_t col)
        {
            return ref.mat[index * ref.cols + col];
        }
        // allows transparent printing of the value referenced by the proxy
        // reading will be something similar, though, obviously, not on a
        // constant proxy
        friend std::ostream & operator <<(std::ostream & out,
                                          const proxy & val)
        {
            T x = val;
            out << x;
            return out;
        }
    };

    // this does the same thing as the basic proxy, but for const matrixes.
    // proxy's ref member cannot reference a const matrix, and this was the
    // best way I could think of to avoid const_casting away const
    template <class U>
    class const_proxy
    {
        const matrix &ref;
        size_t index;
    public:
        const_proxy(const matrix &ref,
                    size_t index): ref(ref), index(index)
        {

        }
        operator T() const
        {
            return ref.mat[index];
        }
        T operator [](size_t col)const
        {
            return ref.mat[index * ref.cols + col];
        }
        friend std::ostream & operator <<(std::ostream & out,
                                          const const_proxy & val)
        {
            T x = val;  // this time everything's const, so we don't need
                        // the extra method
            out << x;
            return out;
        }
    };
    // The following friendship declarations are required before C++11.
    // friend class proxy<T>;
    // friend class const_proxy<T>;

    matrix(int numRows, int numCols) :
            mat(numRows * numCols), rows(numRows), cols(numCols)
    {
    }

    matrix(std::initializer_list<T> initlist) :
            mat( { initlist }), rows(1), cols(initlist.size())
    {
    }

    matrix(std::initializer_list<std::initializer_list<T>> initlist) :
            rows(initlist.size()), cols(initlist.begin()->size())
    {
        for (const auto &rowElement : initlist)
        {
            mat.insert(mat.end(), rowElement.begin(), rowElement.end());
            if (rowElement.size() != cols)
            {
                throw std::runtime_error("Initializer list is not rectangular.");
            }
        }
    }

    // rather than returning the value directly, returns the proxy
    proxy<T> operator[](size_t index)
    {
        return proxy<T>(*this, index);
    }

    const_proxy<T> operator[](size_t index) const
    {
        return const_proxy<T>(*this, index);
    }
};

And usage

np::matrix<int> mat(3, 3);
np::matrix<int> mat1 = { { 1, 2, 3 }, { 4, 5, 6 } };
np::matrix<int> mat2 = { 1, 2, 3, 4, 5, 6 };
const np::matrix<int> mat3 = mat1; // test for const-correctness

std::cout << mat1[1][2] << std::endl;
// what just happened: mat1[1] returned a proxy. [2] is invoked on the proxy
// retrieving the value at mat1[1][2]. << is called with the returned
// value

std::cout << mat1[4] << std::endl;
// what just happened: mat1[4] returned a proxy. << is called on the proxy,
// which in turn retrieves the value at index 4 of our flattened-to-1D array
// << is called with the retrieved value

std::cout << mat3[1][2] << std::endl;
std::cout << mat3[4] << std::endl;

Now... Are you certain you want to inflict this much complexity on people?

user4581301
  • 33,082
  • 7
  • 33
  • 54