Generische Klassen

Content

Nehmen wir als Beispiel folgende Klasse Matrix für Matrizen, die bislang nur für den Elementtyp double ausgelegt ist:

#ifndef MATRIX_HPP
#define MATRIX_HPP

#include <cassert>
#include <cstddef>

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; /* stride between subsequent rows */
   const std::size_t incCol; /* stride between subsequent columns */
   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;
   }

   Matrix(const Matrix&) = delete;
   Matrix& operator=(const Matrix&) = delete;

   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];
   }
};

#endif

Wenn wir gerne eine ähnliche Matrix-Klasse für den Datentyp float hätten, könnten wir den obigen Programmtext kopieren und im gesamten Text double durch float ersetzen. Dies könnte dann klappen, wenn wir anschließend der Matrix-Klasse einen neuen Namen geben würden, um ein Namenskonflikt zu vermeiden.

Die textuelle Replikation von Programmtexten mit minimalen Variationen ist kein guter Ansatz aus der Sicht des Software Engineering. Wenn wir die originale Fassung verbessern, müssten wir diese Änderungen manuell bei all den anderen Varianten nachziehen. Seit den 1970er-Jahren gibt es Programmiersprachen, die die generische Programmierung unterstützen (die erste solche Sprache war CLU, die von Barbara Liskov am MIT entwickelt worden ist). Eine generische Klasse hängt von einem oder mehreren Typparameter ab und kann in generischer Form unter Nutzung dieser Typparameter geschrieben werden. Solche generische Klassen können dann mit konkreten Datentypen instantiiert und danach wie jede andere Klasse genutzt werden.

Generische Konstrukte werden in C++ Templates genannt. Templates können sich in C++ auf Klassen, Funktionen und einige andere Deklarationen beziehen. Im Falle einer Matrix-Klasse würden wir hier nur einen einzigen Parameter T für den Elementtyp benötigen:

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; /* stride between subsequent rows */
   const std::size_t incCol; /* stride between subsequent columns */
   T* data;

   /* ... */

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

Eine Template-Deklaration

Im obigen Beispiel ist Matrix nicht mehr länger eine gewöhnliche Klasse, sondern eine Template-Klasse. Template-Klassen können nur genutzt werden, wenn sie zuvor instantiiert wurden, d.h. die Template-Parameter müssen mit konkreten Typen assoziiert werden. Das kann geschehen, indem die gewünschen Parameter ebenfalls in winkligen Klammern angegeben werden. Im folgenden Beispiel haben wir die Matrizen A und B. A nutzt double als Elementtyp, während B eine Instantiierung von Matrix, bei der float für T eingesetzt wird.

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

Aufgaben