Generische Klassen

Bislang unterstützte die Matrix-Klasse nur den Datentyp double:

struct Matrix {
   const std::size_t m; /* number of rows */
   const std::size_t n; /* number of columns */
   const std::size_t incRow;
   const std::size_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];
   }

   /* ... */
};

Wenn wir analog dazu Interesse an einer Matrix-Klasse hätten für den Datentyp float, könnte die bisherige Klasse übernommen und überall double durch float ersetzt werden. Sie würde funktionieren, dürfte aber nicht mehr struct Matrix genannt werden bzw. müsste einen eigenen Namensraum erhalten, um nicht in Konflikt zur anderen Klasse zu stehen.

Die textuelle Vervielfältigung einer solchen Matrix-Klasse erscheint jedoch sehr unvorteilhaft, weil dann statt einer Fassung viele weitgehend textuell identische Varianten zu pflegen sind. In der Informatik gibt hier schon seit längerer Zeit den Ansatz zur generischen Programmierung. Hierbei wird ein Teil des Programmtexts wie beispielsweise eine Klasse mit einem Typen parametrisiert und entsprechend generisch formuliert. Ein generisches Konstrukt kann dann für einen konkreten Typ instantiiert werden und ist dann in dieser Form nutzbar.

In C++ werden die generischen Programmkonstrukte Templates genannt. Ein Template kann eine Deklaration (wie beispielsweise eine Klasse oder eine Funktion) mit einem oder mehreren Typen parametrisieren. (Es sind auch andere Template-Parameter möglich.) Im Beispiel der Matrix-Klasse würde uns zunächst ein Parameter für den Elementtyp genügen, den wir hier T nennen:

template<typename T>
struct Matrix {
   const std::size_t m; /* number of rows */
   const std::size_t n; /* number of columns */
   const std::size_t incRow;
   const std::size_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];
   }

   /* ... */
};

Eine Template-Deklaration

Nach der obigen Template-Deklaration ist Matrix keine reguläre Klasse mehr, sondern eine Template-Klasse. Nutzbar wird sie erst durch eine Instantiierung, d.h. die Template-Parameter müssen konkretisiert werden. Dies ist möglich, indem innerhalb der Winkel die konkreten Parameter angegeben werden. Im folgenden Beispiel werden die Matrizen A und B deklariert. A verwendet double als Elementtyp, während B auf float basiert.

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

Aufgabe

Verwandeln Sie die struct Matrix der Vorlage in eine Template-Klasse wie oben beschrieben und testen Sie sie für verschiedene Datentypen.

Welche Methode von struct Matrix lässt sich nicht trivial anpassen?

Gibt es Typen, die bei Matrix nicht als Parameter eingesetzt werden dürfen?

Vorlage

#include <cstddef> /* needed for std::size_t */
#include <cstdio> /* needed for printf */
#include <cassert> /* needed for assert */

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::size_t incRow;
   const std::size_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 * n + i + 1;
         }
      }
   }

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

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