Generic classes

Content

Our Matrix class supported just double so far:

struct Matrix {
   const std::size_t m; /* number of rows */
   const std::size_t n; /* number of columns */
   const std::ptrdiff_t incRow;
   const std::ptrdiff_t incCol;
   double* data;

   /* ... */

   const double& operator()(std::size_t i, std::size_t j) const {
      assert(i < m && j < n);
      return data[i*incRow + j*incCol];
   }
   
   double& operator()(std::size_t i, std::size_t j) {
      assert(i < m && j < n);
      return data[i*incRow + j*incCol];
   }

   /* ... */
};

When we are interested in having a similar Matrix class for float, we could copy the program text of the class above and substitute double by float throughout the text. This would work if we would rename the class as the name Matrix has already been taken by the other class for double. Alternatively, we could put a new class named Matrix into a new namespace and thereby avoid the name conflict.

The textual replication of program texts with small variations is not a good approach in terms of software engineering. Whenever we improve the original version, we would have to fix the replicated versions as well. Since the 1970ies we have programming languages that support generic programming (the first one was CLU developed by Barbara Liskov at MIT). A generic class depends on one or more type parameters and can be written in a generic way using these parameters. Generic classes can then be instantiated with concrete types and subsequently used like other classes.

Generic constructs are called templates in C++. A template can be a class or function with one or more type parameters (or even other parameters). In case of a Matrix class we need just one parameter for the element type of a matrix which we name T:

template<typename T>
struct Matrix {
   const std::size_t m; /* number of rows */
   const std::size_t n; /* number of columns */
   const std::ptrdiff_t incRow;
   const std::ptrdiff_t incCol;
   T* data;

   /* ... */

   const T& operator()(std::size_t i, std::size_t j) const {
      assert(i < m && j < n);
      return data[i*incRow + j*incCol];
   }
   
   T& operator()(std::size_t i, std::size_t j) {
      assert(i < m && j < n);
      return data[i*incRow + j*incCol];
   }

   /* ... */
};

A template declaration

In the example above, Matrix is no longer a regular class but a template class. Template classes can be used only when they are instantiated, i.e. the template parameters must be associated with actual types. This can be done by filling in the parameters using angle brackets. In the following example we have matrices A and B. A uses double as element type while B has been declared as an instantiation of Matrix using float as actual parameter for T.

Matrix<double> A(7, 8, StorageOrder::ColMajor);
Matrix<float> B(3, 3, StorageOrder::RowMajor);

Exercise

Sample code from Session #07

#include <cassert> /* needed for assert */
#include <cstddef> /* needed for std::size_t and std::ptrdiff_t */
#include <printf.hpp> /* needed for fmt::printf */

enum class StorageOrder {ColMajor, RowMajor};

struct Matrix {
   const std::size_t m; /* number of rows */
   const std::size_t n; /* number of columns */
   const std::ptrdiff_t incRow;
   const std::ptrdiff_t incCol;
   double* data;

   Matrix(std::size_t m, std::size_t n, StorageOrder order) :
         m(m), n(n),
         incRow(order == StorageOrder::ColMajor? 1: n),
         incCol(order == StorageOrder::RowMajor? 1: m),
         data(new double[m*n]) {
   }

   ~Matrix() {
      delete[] data;
   }

   const double& operator()(std::size_t i, std::size_t j) const {
      assert(i < m && j < n);
      return data[i*incRow + j*incCol];
   }
   
   double& operator()(std::size_t i, std::size_t j) {
      assert(i < m && j < n);
      return data[i*incRow + j*incCol];
   }

   void init() {
      for (std::size_t i = 0; i < m; ++i) {
         for (std::size_t j = 0; j < n; ++j) {
            data[i*incRow + j*incCol] = j * m + i + 1;
         }
      }
   }

   void print() {
      for (std::size_t i = 0; i < m; ++i) {
         fmt::printf("  ");
         for (std::size_t j = 0; j < n; ++j) {
            fmt::printf(" %4.1lf", data[i*incRow + j*incCol]);
         }
         fmt::printf("\n");
      }
   }
};

int main() {
   Matrix A(7, 8, StorageOrder::ColMajor);
   A.init();
   fmt::printf("A =\n"); A.print();
}