6

I am trying to make a fixed-size matrix class. The intent is to make it inherit or utilize a std::array of std::array:

template <size_t Rows, size_t Columns, class T=double>
struct Matrix : public std::array<std::array<T, Columns>, Rows> {
};

I would like to provide an initializer that can automatically deduce the size, like std::array can in C++17 (which I am using). I'm also fine with using a function to make the Matrix instead of using class template argument deduction.

// What I want, but does not work:
Matrix matrix {{1., 2.},
               {3., 4.}};

// Or:
auto matrix = MakeMatrix ({1., 2.},
                          {3., 4.});

I have failed to get either of these to be possible. Instead, only the following works:

// Requires specifying the size and repeating `std::array`, both undesired
Matrix<2,2> mat {
    std::array{1., 2.},
    std::array{3., 4.}
};

// OR this, which requires specifying the size and is generally confusing
Matrix<2,2> mat2 {1., 2., 
                  3., 4.};

I tried using variadic templates, but this too did not appeal the compiler:

template<class... Args>
auto MakeMatrix (Args... args) {
  return Matrix{ std::array {args} ... };
}

// This causes compiler error:
// error: no matching function for call to 'MakeMatrix'
// candidate function [with Args = <>] not viable: requires 0 arguments, but 2 were provided
auto matrix = MakeMatrix ({1., 2.},
                          {3., 4.});

// This causes compiler error
// error: no viable constructor or deduction guide for deduction of template arguments of 'Matrix'
// note: in instantiation of function template specialization 'MakeMatrix<std::__1::array<double, 2>, std::__1::array<double, 2> >'
// note: note: candidate function template not viable: requires 0 arguments, but 2 were provided
auto matrix = MakeMatrix (std::array {1., 2.},
                          std::array {3., 4.});

I've also considered using std::initializer_list<std::initializer_list<T>>, however these do not support fixed size as far as I can tell, and I want the size determined at compile time.

Any thoughts on how to do this, or is it just impossible with the current C++ mechanics?

Peter Moran
  • 295
  • 3
  • 13
  • 5
    Avoid inheriting from std types -- madness goes there. Use composition (encapsulate a std::array is just fine). – wcochran Aug 02 '20 at 00:19
  • 1
    Beware that standard containers were not designed to behave polymorphically. Public inheritance of standard containers exposes you to potential risks. [Link](https://stackoverflow.com/questions/6806173/subclass-inherit-standard-containers). You can inherit privately, have a `std::array` member or if `Matrix` is very thin use a `using` type alias instead. – François Andrieux Aug 02 '20 at 00:19
  • 1
    See [this question](https://stackoverflow.com/questions/6114067/how-to-emulate-c-array-initialization-int-arr-e1-e2-e3-behaviou) for a `make_array` for a 1D std::array; I'd expect it can be generalized to 2D. Generally I'd advise against this kind of stuff though; variadic templates tend to get messy. – jcai Aug 02 '20 at 00:50

2 Answers2

9

The problem is that the compiler can't deduce {} when used as an argument. This works with for initializer_list (for contructors, because of some special rules). But then you are missing the size.

A workaround are built-in arrays:

template <typename T, size_t N>
using Row = const T (&)[N]; // for readability

template <auto Rows, auto Columns, typename T = double>
class Matrix {
public:
  template <typename... Ts, auto N>
  Matrix(Row<Ts, N>... rows) {}
};

template <typename... RowTypes, auto Columns>
Matrix(Row<RowTypes, Columns>...)
    -> Matrix<sizeof...(RowTypes), Columns, std::common_type_t<RowTypes...>>;

You can now construct Matrix exactly as you like:

const auto m = Matrix{{1, 2}, {1, 2}, {1, 2}};

For the final step, initializing std::array with build-in arrays can be tricky. C++20 provides a function, check the link for possible implementations. If you copy that implementation, or have one available to you, you can create the constructor easily, as:

template <auto Rows, auto Columns, typename T = double>
class Matrix {
public:
  template <typename... Ts, auto N>
  Matrix(Row<Ts, N>... rows) {
    data_ = {to_array(rows)...};
  }
private:
  std::array<std::array<T, Columns>, Rows> data_;
};

Live example, with operator[] to show that the data layout is correct.

Peter Moran
  • 295
  • 3
  • 13
local-ninja
  • 1,198
  • 4
  • 11
  • This is an amazing solution! I'm curious if you learned this from anywhere in particular so I could read about it more. – Peter Moran Aug 02 '20 at 17:36
  • [This proposal](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1591) explains how deduce size and type from an array. I think everything else is _normal_ template programming. For more details check out [this book](https://www.amazon.com/C-Templates-Complete-Guide-2nd/dp/0321714121), it covers nearly everything regarding templates (includes several parts referring to array deduction). – local-ninja Aug 02 '20 at 20:38
0

A simpler alternative using just user-defined deduction guides:

template <size_t Rows, size_t Columns, class T=double>
struct Matrix : public std::array<std::array<T, Columns>, Rows> {
};

template <size_t Rows, size_t Columns, class T>
Matrix(const T(&)[1][Rows][Columns]) -> Matrix<Rows, Columns, T>;

Matrix matrix {{{{1., 2.},
                 {3., 4.}}}};

The disadvantage is that it requires a comically large number of brackets to work.

LHLaurini
  • 1,737
  • 17
  • 31