3

I have a Matrix class

template<typename Type, size_t Rows, size_t Columns> class Matrix
{
public:
    Type Entries[Rows][Columns];

    // Operators, etc.
};

I then needed to make specializations of this class (for square matrices, etc.), which means I needed to rewrite the whole class implementation for each specialization. I then proceeded to think of methods that will avoid code duplication.

First idea that came to mind was to define the specialized functions in the base template but delete them so they can't be used in the base template and define specializations outside the class.

// Inside class definition
auto GetDeterminant() const -> T = delete;

// Outside class definition
template<typename Type, size_t Size> Matrix<T, Size, Size>::GetDeterminant() const -> T
{
    // Implementation
}

But then, after encountering some errors, I learned that partially specializing function templates is not allowed. So I tried these methods

Method 1:

template<typename Derived, typename Type, size_t Rows, size_t Columns> class MatrixBase
{
public:
    T Entries[Row][Columns];

    // Operators, etc.
}

template<typename Type, size_t Rows, size_t Columns> class Matrix : public MatrixBase<Matrix<Type, Rows, Columns>, Rows, Columns>
{
    // General MxN matrix stuff
}

template<typename Type, size_t Size> class Matrix<T, Size, Size> : public MatrixBase<Matrix<T, Size, Size>, Size, Size>
{
public:
    // Specific MxM matrix stuff
}

Here I implemented CRTP as seen on another question, however I feel that the abstraction is somewhat unneeded and though that there is probably a better way. So I came up with another method.

Method 2:

template<typename T, size_t Rows, size_t Columns> class Matrix
{
public:
    T Entries[Rows][Columns]

    auto GetDeterminant() const -> T
    {
        static_assert(Rows == Columns);

        // Some computations
    }
}

Which seems fine, if someone tries to use this on a non-square matrix they get a compile-time error.

Question:

Is there a way of solving this by using only templates? or should I just stick with one of these methods?

max66
  • 65,235
  • 10
  • 71
  • 111

2 Answers2

0

I propose a variant of method 2 using SFINAE to enable or disable GetDeterminant()

template <typename Type, size_t Rows, size_t Columns>
class Matrix
 {
   public:
      Type Entries[Rows][Columns];

      template <size_t R = Rows>
      typename std::enable_if<R == Columns, Type>::type GetDeterminant () const
       {
         // some computations
         return {};
       }
 };

If you want avoid the risk that GetDeterminant() could be anabled expliciting the template R value as follows

Matrix<int,  3, 3>  m0;
Matrix<long, 3, 2>  m1;

auto d0 = m0.GetDeterminant(); // compile
//auto d1 = m1.GetDeterminant(); // compilation error
auto d1 = m1.GetDeterminant<2>(); // compile!!!

You can maintain a static_assert (something like static_assert(R == Rows, "!");) or you can improve the std::enable_if test as follows

  template <size_t R = Rows>
  typename std::enable_if<(R == Columns) && (R == Rows), Type>::type
      GetDeterminant () const
   {
     // some computations
     return {};
   }
max66
  • 65,235
  • 10
  • 71
  • 111
0

First off, quoting aschelper's comment:

I like the static_assert method.

Me too. It's clear, concise and further can be given a meaningful and explaining error message.


helper struct (in implementation namespace)

namespace matrix_impl {
  template<std::size_t Rows, std::size_t Columns>
  struct determinate_calculation {
    int operator()(int const (& entries)[Rows][Columns]) const = delete;
  };
  // or just don't define the class template

  template<std::size_t Size>
  struct determinate_calculation<Size, Size> {
    int operator()(int const (& entries)[Size][Size]) const {
      return entries[0][0]; // Woah, calculation wrong as hell ;)
    }
  };
}

template<std::size_t Rows, std::size_t Columns>
struct Matrix {
  int entries[Rows][Columns];

  int get_determinate() const {
    return matrix_impl::determinate_calculation<Rows, Columns>{}(entries);
  }
};
  • (-1) Needs a class template definition including deleted function, or a class template declaration. This is just boilerplate.
  • (-1) Needs a class template specialization containing the actual code in some function. Much more than writing a single function.
  • (-1/+1) Needs to pass required members of the Matrix class as parameters to the function. Good because it makes dependencies explicit. Bad because with lots of members this can become a hassle (and "hey, let's use a class for parameter passing" just adds to the boilerplate).

std::enable_if

max66 was faster, nothing more to add.

Community
  • 1
  • 1
Daniel Jour
  • 15,896
  • 2
  • 36
  • 63