Search code examples
c++matrixconstexprinitializer-listcompile-time

How to deduce the two dimensions of a compile-time matrix class with an initializer_list constructor?


#include <iostream>
#include <array>

template <typename T, size_t Rows, size_t Cols>
class Matrix {

protected:

    std::array<std::array<T, Cols>, Rows> matrix;

public:

    constexpr Matrix() = default;

    constexpr explicit Matrix(std::array<std::array<T, Cols>, Rows> matrix) : matrix(std::move(matrix)) { }

    consteval Matrix(std::initializer_list<std::initializer_list<T>> matrix) : matrix() {
        if (matrix.size() != Rows) {
            throw std::invalid_argument("Invalid matrix Rows count");
        }
        auto current_row = matrix.begin();
        for (size_t i = 0; i < matrix.size(); i++, current_row++) {
            if (current_row->size() != Cols) {
                throw std::invalid_argument("Invalid matrix column count");
            }
            std::copy(current_row->begin(), current_row->end(), this->matrix[i].begin());
        }
    }

    constexpr auto& operator[](this auto&& self, size_t index) { return self.matrix[index]; }
};

template <typename T, size_t N>
struct Vector : Matrix<T, 1, N> {
    using Matrix<T, 1, N>::Matrix;

    template <typename U, typename... Us>
    consteval explicit Vector(U u, Us... us) {
        this->matrix[0] = { u, us... };
    }

    constexpr auto& operator[](this auto&& self, size_t index) { return self.matrix[0][index]; }
};

template <typename T, typename... U>
Vector(T, U...) -> Vector<T, 1 + sizeof...(U)>;

int main() {
    constexpr Matrix<int, 4, 3> matrix {
        { 1, 2, 3 },
        { 1, 2, 3 },
        { 1, 2, 3 },
        { 1, 2, 3 }
    };
    static_assert(matrix[2][1] == 2);

    constexpr Vector v { 1, 2, 3 };
    static_assert(v[1] == 2);
}

Here, it's quite easy to write a deduction guide for a vector class to avoid specifying the type and the number of elements, but when it comes to the Matrix class, it's quite challenging. This is because std::initializer_list::size for the passed initializer_list is not a constant expression, and I can't deduce the Cols argument from it.

  1. How can one write such an expression and have the type and the dimensions deduced?
    constexpr Matrix matrix {
        { 1, 2, 3 },
        { 1, 2, 3 },
        { 1, 2, 3 },
        { 1, 2, 3 }
    };
  1. How can one write such an expression and have the dimensions deduced and the type of contained elements be the initializer elements converted to the passed type?
    constexpr Matrix<double> matrix {
        { 1, 2, 3 },
        { 1, 2, 3 },
        { 1, 2, 3 },
        { 1, 2, 3 }
    };

Solution

  • You could add a deduction guide for arrays:

    template <class T, size_t R, size_t C>
    Matrix(const T(&arr)[R][C]) -> Matrix<T, R, C>;
    

    ... and you'll need an extra set of braces to use it:

    constexpr Matrix matrix {{
        { 1, 2, 3 },
        { 1, 2, 3 },
        { 1, 2, 3 },
        { 1, 2, 3 }
    }};
    

    Alternatively, replace the constructor taking nested initializer_lists with constructors taking arrays. That makes your Matrix class work for types that are moveable but neither copyable nor default constructible and it does not need deduction guides for arrays:

    template <typename T, size_t Rows, size_t Cols>
    class Matrix {
    protected:
        std::array<std::array<T, Cols>, Rows> matrix;
    
        constexpr auto to_array(const T (&arr)[Rows][Cols]) {
            return [&]<std::size_t... Rs>(std::index_sequence<Rs...>) {
                return std::to_array({std::to_array<T>(arr[Rs])...});
            }(std::make_index_sequence<Rows>{});
        }
        constexpr auto to_array(T (&&arr)[Rows][Cols]) {
            return [&]<std::size_t... Rs>(std::index_sequence<Rs...>) {
                return std::to_array({std::to_array<T>(std::move(arr[Rs]))...});
            }(std::make_index_sequence<Rows>{});
        }
    
    public:
        constexpr Matrix()
            requires((Rows == 0 && Cols == 0) || std::is_default_constructible_v<T>)
        = default;
    
        constexpr explicit Matrix(const T (&arr)[Rows][Cols])
            : matrix(to_array(arr)) {}
    
        constexpr explicit Matrix(T (&&arr)[Rows][Cols])
            : matrix(to_array(std::move(arr))) {}
    
        //...
    };