Different template class realizations but same member functions

Viewed 117

I have a template matrix class with some typical member functions such as inverse, determinant, operator*, etc.. And I want to reuse the code for these member functions in template realizations for both fixed- and dynamic size matrices. Is this possible? and if yes, how?

In the code below, like Eigen, I use "-1" for dynamic dimensions. Yes I know I could use a library for this, but for my application, that is not feasible. The standard functionality is not possible due to the nature of the application (CUDA)

Is it possible to make a template class with different member variable size depending on the template parameters? For instance when Dynamic_Rows = -1 and Dynamic_Cols = -1, then the data is just T **data, but otherwise its T data[Rows][Cols].

As of now, I have one template class for dynamic size matrices ("minimal" example below, note that the code is subject to errors/mistakes because I am relatively fresh in "advanced" class templating).

But I want to have a fixed size data member variable in the case of a fixed size matrix instantiation.

template<class T, int Dynamic_Rows, int Dynamic_Cols>
class CMatrix<T, -1, -1>
{
private:
   size_t n_rows, n_cols;
   T** data;

   void allocate_data();
   void deallocate_data();

public:
   CMatrix(const size_t n_rows, const size_t n_cols);
   CMatrix(const CMatrix& other);
   ~CMatrix();
   CMatrix& operator=(const CMatrix& rhs);
   CMatrix exp() const;
};

with for instance the code for the exp() function below as

template <class T, int Dynamic_Rows, int Dynamic_Cols>
CMatrix<T, -1, -1> CMatrix<T, -1, -1>::exp() const
{
   CMatrix<T, -1, -1> result(n_rows, n_cols);
   for (size_t i = 0; i < n_rows; i++)
   {
      for (size_t j = 0; j < n_cols; j++)
      {
         result.data[i][j] = exp(result.data[i][j]);
      }
   }
   return result;
}

The only thing I can think of now to allow for both dynamic and fixed-size matrices is to basically implement another template of the class as

template<class T, size_t Rows, size_t Cols>
class CMatrix<T, Rows, Cols>
{
private:
   size_t n_rows = Rows, n_cols = Cols;
   T data[Rows][Cols];

public:
   CMatrix() {}
   CMatrix(const CMatrix& other);
   CMatrix& operator=(const CMatrix& rhs);
   CMatrix exp() const;
};

using a variadic template

template<class T, int...> class CMatrix;

but then that would just duplicate the code for most of my member functions!

4 Answers

This is a very generic way to do such specializations using CRTP. It doesn;t require that there is a simple single condition that can be used to select an implementation of a data member, and leave the rest unchanged.

template <class T, class Impl> class MatrixBase {

   // data is NOT defined here
   Impl exp() const
   {
        Impl& self = static_cast<Impl&>(*this);             // <-- this is magic
        Impl result = self;                                 // <-- this is magic
        for (size_t i = 0; i < n_rows; i++)
        {
            for (size_t j = 0; j < n_cols; j++)
            {
                result.data[i][j] = exp(result.data[i][j]); // <-- data is defined in Impl
            }
        }
        return result;
    }
    // other functions that do not depend on actual type of data
};

template <class T>
class DynamicMatrix : public MatrixBase<T, DynamicMatrix<T>> {
    T** data;
    // define constructors etc here
};

template <class T, int Rows, int Cols>
class StaticMatrix : public MatrixBase<T, StaticMatrix<T, Rows, Cols>> {
    T[Rows][Cols] data;
    // define constructors etc here
};

Now that you have both StaticMatrix and DynamicMatrix, you can unify them into a single alias template if you wish.

template <class T, int Rows, int Cols>
using CMatrix = std::conditional_t <(Rows >= 0 && Cols >= 0),
                                     StaticMatrix<T, Rows, Cols>,
                                     DynamicMatrix<T>
                                   >;

Is it possible....[...] ...when Dynamic_Rows = -1 and Dynamic_Cols = -1, then the data is just T **data, but otherwise its T data[Rows][Cols]?

You can provide a trait type whose specializations gives you the proper type for the CMatrix

template<typename T, int Rows, int Cols> struct type_helper final {
    static_assert(Rows >= 0 && Cols >= 0, "Rows and COls must be valid!");
    using type = T[Rows][Cols];
};

template<typename T> struct type_helper<T, -1, -1>  final {
    using type = T**;
};

Now in the class

template<class T, int Dynamic_Rows = -1, int Dynamic_Cols = -1>
class CMatrix /* final */
{
    // get the member Type like this!
    using Type = typename type_helper<T, Dynamic_Rows, Dynamic_Cols>::type;

    Type data;
public:

};

(See a demo online)

You can use std::conditional_t to select one type or another based on some compile-time constant, without duplicating the class.

However - I did something very similar once, and found that using a fixed size data structure (std::array instead of std::vector) gives little to no improvement in practical use cases. A very big advantage of fixed size arrays (std::array or type[fixed_size]) is that the compiler is able to optimize out a lot of operations, but if the arrays are large the benefit can become negligible.

Also, you should consider n. 'pronouns' m.'s comment - especially if you're using a GPU (they tend to require data structures that are continuous in memory). Instead of using T** data, use a single std::vector<T>, and an additional std::array<size_t, number_of_dimensions> for size in each dimension. Then you can translate indices with something like this:

using Indices = std::array<size_t, number_of_dimensions>;

template <unsigned int n = number_of_dimensions>
static inline size_t indices_to_offset(Indices const & indices,
                                       Indices const & dimensions){
    if constexpr (n == 0) return 0;
    else {
        size_t const next_index_number = indices[n-1];
        assert(next_index_number < dimensions[n-1]);
        return next_index_number +
            dimensions[n-1]*indices_to_offset<n-1>(indices, dimensions);
    }
}

template <unsigned int n = number_of_dimensions>
static inline Indices offset_to_indices(size_t const offset,
                                        Indices const & dimensions,
                                        Indices indices = Indices()){
    if constexpr (n == 0) return indices;
    else {
        size_t const fast_dimension_size = dimensions[n-1];
        indices[n-1] = offset % fast_dimension_size;
        return offset_to_indices<n-1>(offset/fast_dimension_size , dimensions, indices);
    }
}

Turning my comment into answer (using Vector instead of Matrix):

split your class, so you have the part to specialize in a specific class, and the common part, something like:

// helper to allow variadic constructor without pitfall.
constexpr struct in_place_t{} in_place{};

template <typename T, std::size_t DIM>
class VectorData
{
protected: // You might even make it private and give the common accessor in public part
           // To avoid to use interface not shared with std::vector
    std::array<T, DIM> data;
public:
    VectorData() = default;

    template <typename ... Ts>
    VectorData(in_place_t, Ts&&... args) : data{std::forward<Ts>(args)...} {}
};

template <typename T>
class VectorData<T, std::size_t(-1)>
{
protected: // You might even make it private and give the common accessor in public part
           // To avoid to use interface not shared with std::array
    std::vector<T> data;
public:
    explicit VectorData(std::size_t size) : data(size) {}

    template <typename ... Ts>
    VectorData(in_place_t, Ts&&... args) : data{std::forward<Ts>(args)...} {}
};

Then the common part:

template <typename T, std::size_t DIM = std::size_t(-1)>
class Vector : public VectorData<T, DIM>
{
public:
    using VectorData<T, DIM>::VectorData;

    Vector(std::initializer_list<T> ini) : VectorData<T, DIM>(in_place, ini) {}

#if 1 // Those might go in `VectorData` to avoid to use `data` directly
    std::size_t size() const { return this->data.size(); }
    T& operator[](std::size_t i) { return this->data[i]; }
    const T& operator[](std::size_t i) const { return this->data[i]; }
#endif

    // Example of methods which don't care underlying container
    template <std::size_t DIM2>
    Vector& operator += (const Vector<T, DIM2>& rhs) {
        assert(this->size() == rhs.size());
        
        for (std::size_t i = 0; i != this->size(); ++i) {
            (*this)[i] += rhs[i];
        }
        return *this;
    }

    template <std::size_t DIM2>
    friend Vector operator + (Vector lhs, const Vector<T, DIM2>& rhs) {
        return lhs += rhs;
    }
};

Demo

Related