Zugriffmethoden auf ein einzelnes Element

Es erscheint sinnvoll, einfache Zugriffsmethoden anzubieten, um auf ein einzelnes Element einer Matrix lesenderweise oder schreibenderweise zuzugreifen.

Entsprechende Methoden könnten wie folgt aussehen:

struct Matrix {
   /* ... */

   double get(std::size_t i, std::size_t j) const {
      return data[i*incRow + j*incCol];
   }

   void set(std::size_t i, std::size_t j, double value) {
      data[i*incRow + j*incCol] = value;
   }

   /* ... */
};

Das const hinter der Parameterliste bei der get-Methode bedeutet, dass die entsprechende Methode den Zustand der Matrix nicht verändert. Sie kann auch dann aufgerufen werden, wenn nur eine Referenz auf eine Matrix mit Leserechten vorliegt, also beispielsweise const Matrix&.

Die Nutzung würde dann beispielsweise so aussehen:

if (A.get(2, 3) > 0) {
   A.set(2, 3, -1);
}

In C++ geht dies jedoch auch sehr viel eleganter unter Verwendung von Referenzen. Nicht nur Parameter können einen Referenzparameter haben, auch für den Rückgabewert ist dies zulässig. Wenn eine Referenz zurückgegeben wird, kann der Methodenaufruf sowohl auf der linken als auch auf der rechten Seite einer Zuweisung stehen:

struct Matrix {
   /* ... */

   double& access(std::size_t i, std::size_t j) {
      return data[i*incRow + j*incCol];
   }

   /* ... */
};

So könnte dann die Nutzung aussehen:

if (A.access(2, 3) > 0) {
   A.access(2, 3) = -1;
}

Somit kann A.access(i, j) wie jede andere Variable auch verwendet werden.

In C++ wird die Schreibweise typischerweise mit Hilfe des Überladens des Funktionsoperators noch weiter vereinfacht. Vom Überladen wird gesprochen, wenn der gleiche Operator für verschiedene Datentypen verwendet werden kann. So ist der Operator + sowohl für int als auch double zulässig und funktioniert in jeweils unterschiedlich. In C++ ist es möglich, diese Überladungen auch auf eigene Datentypen zu erweitern. Dies geht für sämtliche Operatoren von C++. Auch der Funktionaufruf, d.h. der Operator () lässt sich überladen. Überladene Operatoren können wie normale Funktionen oder Methoden definiert werden, nur wird der Name dann durch operator und dem jeweiligen Operatorsymbol ersetzt. So lässt sich die access-Methode durch einen Operator ersetzen:

struct Matrix {
   /* ... */

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

   /* ... */
};

Die Nutzung wäre dann so möglich:

if (A(2, 3) > 0) {
   A(2, 3) = -1;
}

Wenn diese Zugriffsmethode mit Referenz sowohl bei schreibbaren als auch nur zum Lesen zur Verfügung stehenden Matrizen benutzbar sein soll, benötigen wir beide Varianten:

struct Matrix {
   /* ... */

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

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

   /* ... */
};

Der Übersetzer wählt dann vollautomatisch das aus, was jeweils benötigt wird.

Wenn Sie möchten, können Sie die Zugriffsmethoden auch mit einer Überprüfung der Indizes ausstatten. Dies geht mit Hilfe der Funktion assert aus #include <cassert>:

#include <cassert>
/* ... */
struct Matrix {
   /* ... */

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

   /* ... */
};

Aufgabe

Ergänzen Sie die Vorlage durch eine Kopierfunktion mit folgender Signatur und verwenden Sie dazu die oben definierten Zugriffsmethoden:

void copy_matrix(const Matrix& A, Matrix& B) {
   /* copy A to B */
}

Überlegen Sie sich oder ggf. testen Sie, welche der Zugriffsmethoden bei welcher der Matrizen hierfür aufgerufen werden.

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]) {
   }

   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();
   A(2, 3) = -1;
   std::printf("A =\n"); A.print();
}