C++-Programmierung/ Eine Matrix-Bibliothek – mitrax/ Die Matrixklasse

Aus Wikibooks
Zur Navigation springen Zur Suche springen


Das matrix-Klassentemplate ist das Herzstück von mitrax. Alle Inhalte dieses Kapitels stehen in der Datei matrix.hpp. Wir haben bereits festgestellt, dass eine Matrix aus einer Dimension und den entsprechend angeordneten Elementen besteht. Einen Datentyp für die Dimension haben wir uns im letzten Kapitel geschaffen. Den Datentyp für die Elemente wollen wir nicht selbst vorgeben, daher ist dies ein Templateparameter. Somit bleibt noch die Frage, wie wir unsere Elemente speichern. Was wir benötigen, ist im Grunde, ein zweidimensionales Array. Da uns die Standardbibliothek keinen entsprechenden Container zur Verfügung stellt, werden wir uns selbst um die zweidimensionale Anordnung unserer Elemente kümmern müssen. Da wir einen schnellen Zugriff auf alle Elemente wünschen, werden wir einen Container wählen, der Random-Access-Iteratoren anbietet. Anhand dieser Überlegungen erscheint std::vector geeignet.

Klassendeklaration[Bearbeiten]

Sie werden sich möglicherweise fragen, warum wir uns überhaupt auf einen spezifischen Container festlegen sollten. Wir könnten den Container schließlich genauso gut als Templateparameter festlegen und dem Nutzer somit die Möglichkeit, für besondere Optimierungen anbieten. Tatsächlich werden wir dies auch machen, aber es ergibt sofort ein Problem dadurch. Unsere matrix-Klasse sollte möglichst einfach zu Handhaben sein. Wenn ein Benutzer also eine Matrix aus float-Elementen wünscht, dann wird er sich in den meisten Fällen, keine Gedanken darüber machen wollen, in welchem Container die Elemente innerhalb der Matrix gespeichert sind. Wir verpassen dem zweiten Parameter daher als Standardwert einen std::vector.

Nuvola-inspired-terminal.svg
1 template < typename Type, template< typename > class Container = std::vector >
2 class matrix;

Wenn Sie versuchen diese Deklaration zu machen, wird sich ihr Compiler beschweren, dass die Templateargumentliste von std::vector nicht kompatibel zur angegeben Liste ist. Dort steht, dass Container nur ein Templateargument erhalten soll, std::vector bekommt jedoch zwei Argumente. Das Erste ist natürlich der Datentyp der Elemente, als Zweites kann jedoch noch ein spezieller Allocator angegeben werden. Da dieser einen Standardwert besitzt, fällt dies normalerweise nicht auf, aber an dieser Stelle prüft C++ nicht, ob die Deklarationen, unter Berücksichtigung der Standardargumente, kompatibel sind. Wir müssten also für den Container zwei Templateargumente spezifizieren und innerhalb der Definition von matrix, manuell den Standard-Allocator angeben. Dies hat jedoch gleich zwei Nachteile. Zum einen ist matrix dadurch sofort inkompatibel zu allen Containern, die nicht als ersten Parameter den Typ für die Elemente und als Zweiten den Typ für einen Allocator übernehmen. Zum anderen können wir keinen eigenen Allocator mehr spezifizieren.

Dies schränkt die Möglichkeiten zu optimieren durch einen eigenen Container viel zu stark ein. Wir benötigen eine andere Strategie, um den Container zu spezifizieren. Eine für uns einfache Möglichkeit bestünde darin, nur den Container angeben zu lassen und den Typ der Daten dann aus diesem zu extrahieren. Typischerweise existiert innerhalb eines Container ein typedef auf den Datentyp, den die Elemente haben. Das Ganze könnte dann etwa so aussehen.

Nuvola-inspired-terminal.svg
1 template < typename Container >
2 class matrix{
3     // Typ von dem unsere Elemente sind
4     typedef typename Container::value_type type;
5 };

Wir haben jedoch bereits erläutert, dass der Container in irgendeiner Weise, standardmäßig std::vector sein sollte. Bei diesem Vorgehen haben wir keine Chance dies anzugeben. Der Nutzer müsste den Container immer selbst mit angeben. Hinzu kommt natürlich noch, dass der Container-Typ einen typedef namens value_type anbieten muss. Dies ist nicht sonderlich kritisch, aber die Abhängigkeiten sollten dennoch möglichst gering gehalten werden. Glücklicherweise gibt es einen Mechanismus, der es uns erlaubt, den Datentyp der Elemente anzugeben und diesen auch gleich als Argument für einen Standardcontainer zu verwenden.

Nuvola-inspired-terminal.svg
1 template < typename Type, typename Container = std::vector< Type > >
2 class matrix;

Jetzt muss der Nutzer zwar zweimal den Typ für die Elemente angeben, wenn er einen anderen Container wünscht, aber im Gegenzug kann er auch wirklich jeden beliebigen Container benutzen, der Elemente vom Typ Type speichert. Wenn der Nutzer keinen eigenen Container wünscht, muss er nur den Datentyp für die Elemente angeben. Die Nachteile halten sich also in Grenzen und treten insbesondere auch nur dann zu Tage, wenn der Nutzer ohnehin Sonderwünsche hat. Typischerweise wir er aufgrund dessen, ohnehin in die Dokumentation der matrix-Klasse schauen müssen, um dort zu erfahren, wie in Zusammenhang mit dem Containertypen optimiert werden kann. Diese Technik wird übrigens auch in der Standardbibliothek verwendet. Sehen Sie sich beispielsweise mal den Container-Adapter std::stack an.

Falls Sie sich nun Fragen, wie diese Optimierungsmöglichkeiten denn aussehen könnten, dann denken Sie noch einmal an den Allocator von std::vector zurück. Dieser ist eine solche Optimierungsmöglichkeit. Eine andere bestünde darin, einen Container zu verwenden, der spätes Kopieren unterstützt. Bei dieser Technik werden die Daten bei einer Zuweisung nicht sofort kopiert. Stattdessen, verweist jeder Container nur auf ein Datenpaket und dieses speichert, wie viele Container derzeit auf es verweisen. Solange kein Container schreibend auf die Daten zugreift, spricht nichts dagegen, wenn mehrere Container sich die gleichen Daten teilen. Eine Kopie muss erst ausgeführt werden, wenn ein Container schreiben auf Daten zugreift, die gerade von mehr als einem Container verwendet werden. Dies kann in bestimmten Anwendungsfällen zu erheblichen Beschleunigen führen.

Den Datentyp der Dimension können wir auf dimension< std::size_t > oder einen anderen unsigned int-Typen festlegen. Es würden sich keine praktisch relevanten Vorteile daraus ergeben, den Datentyp für die Anzahl der Zeilen und Spalten variablen zu gestalten. Negative Angaben sind nicht Sinnvoll und die Wahl eines kleineren unsigned-Typs ist praktisch ebenfalls irrelevant. Im Vergleich zur Größe der Elemente, fällt der hier genutzte Speicherplatz nicht ins Gewicht. Falls der Nutzer ohnehin nur mit sehr kleinen Matrizen arbeiten will, wird er sich nicht für die Verwendung vom mitrax entscheiden, sondern eine Bibliothek wählen, bei der er den Overhead für beliebig große Matrizen nicht zahlen muss.

Klassendefinition[Bearbeiten]

Das Grundgerüst für unsere Implementierung sieht folgendermaßen aus:

Nuvola-inspired-terminal.svg
1 template < typename T, typename Container = std::vector< T > >
2 class matrix{
3 public:
4     // ... folgt noch
5 private:
6     dimension_type dimension_;
7     container_type data_;
8 };

Als Erstes wollen wir uns Gedanken darüber machen, welche Typen unsere Klasse nach außen hin anbieten sollte. Sie erkennen an der Deklaration der Datenmember bereits, dass die Typen für die Dimension und den Container mittels typedef definiert sein müssen. Weiterhin benötigen wir natürlich Zugriff auf den Datentyp der Elemente. Bei dieser Gelegenheit, werden wir auch gleich, analog zu den Containerklassen, typedefs für eine Referenz und eine Referenz auf const für die Elemente einführen. Neben dem Datentyp für die Dimension, sollten wir auch den Datentyp der Dimensionselemente verfügbar machen. Dies erleichtert die Lesbarkeit. Der Container verwaltet intern ebenfalls Elemente, daher sollte er auch einen size_type anbieten. Diesen werden wir einfach von der Dimension übernehmen.

Da wir ein Iteratorinterface anbieten wollen, müssen wir die typedefs iterator und const_iterator anbieten. Zusätzlich benötigen wir einen difference_type, der die Entfernung zwischen zwei Iteratoren wiedergibt. Auch hier bedienen wir uns beim Container, denn dieser muss ebenfalls alle drei anbieten. Schließlich benötigen wir noch die Typen der Proxyklassen, die wir für den Zugriff auf Zeilen und Spalten verwenden. Natürlich ist auch hier je eine konstante und eine nicht-konstante Variante notwendig. Die Datentypen, die hinter diesen typedefs stehen werden wir später noch schreiben. Für den Moment sollten Sie annehmen, dass diese bereits existieren. Das Ganze sieht dann folgendermaßen aus:

Nuvola-inspired-terminal.svg
 1 // Daten
 2 typedef T                                    value_type;
 3 typedef T&                                   reference;
 4 typedef T const&                             const_reference;
 5 typedef Container                            container_type;
 6 
 7 // Dimension
 8 typedef typename Container::size_type        size_type;
 9 typedef mitrax::dimension< size_type >       dimension_type;
10 
11 // Iteratoren
12 typedef typename Container::iterator         iterator;
13 typedef typename Container::const_iterator   const_iterator;
14 typedef typename Container::difference_type  difference_type;
15 
16 // Elementzugriff
17 typedef mitrax::row_proxy< matrix >          row_proxy;
18 typedef mitrax::row_const_proxy< matrix >    row_const_proxy;
19 typedef mitrax::column_proxy< matrix >       column_proxy;
20 typedef mitrax::column_const_proxy< matrix > column_const_proxy;

Sicher werden Sie sich fragen, warum wir für dimension und die Proxyklassen explizit den Namensraum mitrax spezifizieren. matrix steht schließlich selbst in diesem Namensraum. Die Antwort ist für die Proxys bei genauerem hinsehen sofort ersichtlich. Wir überschreiben innerhalb unserer Klasse den Namen mit einer neuen Bedeutung. So ist beispielsweise row_proxy innerhalb unserer Klasse ein typedef auf irgendetwas und folglich kann der Compiler das Klassentemplate row_proxy, welches außerhalb der Klasse matrix deklariert ist, nicht mehr finden. Daher müssen wir dem Compiler an dieser Stelle sagen, dass er im Namensraum mitrax nach dem Bezeichner row_proxy suchen soll. Wir werden in Kürze eine Funktion namens dimension einführen, womit auch dieser Bezeichner überdeckt wird.

Konstruktoren[Bearbeiten]

Wir werden mehrere Konstruktoren anlegen. Als Erstes natürlich einen Standardkonstruktor, der eine Matrix mit null Zeilen und Spalten definiert. Dann einen Konstruktor, der eine Matrix durch Angabe einer Dimension erstellt und dabei alle Elemente standardkonstruiert. Sinnvollerweise werden wir diese Variante als Spezialfall (Standardparameter) eines Konstruktors implementieren, der alle Elemente mit einem vorgegeben Wert belegt. Letztlich brauchen wir noch einen Konstruktor, der eine schnelle Initialisierung bei gegebenen Elementen gewährleistet. Die beiden Konstruktoren, die eine Dimension übernehmen, werden wir jeweils in zwei Ausführungen implementieren. Die Erste bekommt ein dimension-Objekt und die Zweite bekommt Zeilen und Spalten getrennt. Dies ist lediglich eine Vereinfachung für den Benutzer, da beide Varianten häufig genutzt werden.

Für die effiziente Elementinitialisierung machen wir uns die swap()-Funktion zu nutzte. Ein Container besitzt intern immer einen Zeiger auf seine Daten und entsprechend ist die swap-Funktion für gewöhnlich so überladen, dass nur die internen Zeiger ausgetauscht werden müssen. Der Nutzer kann also einen eigenen Container mit den gewünschten Daten füllen und anschließend sein Containerobjekt gegen das leere Objekt in der Matrixklasse austauschen. Dies bringt natürlich den Nachteil mit sich, dass der Nutzer für die Belegung seines Containers, die zweidimensionale Position selbst berechnen muss. Wir werden später eine Hilfsklasse konstruieren, die dieses Problem ein wenig kompensiert. Falls die Größe des übergebenen Containers nicht zur Dimension passt, werden wir eine Ausnahme werfen. Die Unannehmlichkeiten, die anschließend noch für den Nutzer verbleiben, sind der Preis, den er zahlen muss, wenn er eine effiziente Vorbelegung seiner Elemente wünscht.

Gewöhnlich würden wir die Definition nicht innerhalb der Klasse vornehmen, aber um etwas Platz zu sparen, werden wir dies innerhalb der Kapitel tun. In der Quelltextübersicht, am Ende dieses Abschnitts, finden Sie Deklaration und Definition getrennt. Unserer Konstruktoren sehen somit folgendermaßen aus:

Nuvola-inspired-terminal.svg
 1 public:
 2     matrix(){}
 3 
 4     matrix(dimension_type const& size, const_reference fill = value_type()):
 5         dimension_(size),
 6         data_(elements(), fill)
 7         {}
 8 
 9     matrix(size_type const& rows, size_type const& columns, const_reference fill = value_type()):
10         dimension_(dimension_type(rows, columns)),
11         data_(elements(), fill)
12         {}
13 
14     matrix(dimension_type const& size, container_type& data):
15         dimension_(size)
16     {
17         raw_data_swap(dimension_, data);
18     }
19 
20     matrix(size_type const& rows, size_type const& columns, container_type& data):
21         dimension_(dimension_type(rows, columns))
22     {
23         raw_data_swap(dimension_, data);
24     }
25 
26 private:
27     void raw_data_swap(dimension_type const& size, container_type& data){
28         if(elements(size) != data.size()){
29             throw error::data_size("mitrax::matrix<>::raw_data_swap", size, data);
30         }
31         using std::swap;
32         swap(data_, data);
33     }

Wie Sie sehen, wurde das Austauschen der Container in eine private Methode ausgelagert. Da wir dies sowieso im Funktionsrumpf erledigen, haben wir keine Laufzeiteinbußen und wir werden diese Funktion später noch einmal an anderer Stelle verwenden. Den Datentyp error::data_size werden wir später im Kapitel über Ausnahmebehandlung einführen. Falls Sie das Ganze schon mal testen möchten, können Sie auch einfach einen std::logic_error werfen.

Der Standardkonstruktor setzt voraus, dass der Container standardkonstruierbar ist. Die nachfolgenden beiden Konstruktoren verlangen, dass der Container über einen Konstruktor verfügt, der eine Größe und ein Füllelement übernimmt. Die letzten beiden Konstruktoren erwarten, dass für den Container die swap()-Funktion überladen ist oder dass der Container standardkonstruierbar und kopierzuweisbar ist. Letzteres wird benötigt, falls wider Erwarten auf std::swap() zurückgegriffen werden muss. In diesem Fall sind die Konstruktoren jedoch sehr wahrscheinlich ineffizient.

Iteratorinterface[Bearbeiten]

Das Iteratorinterface der Matrixklasse macht uns nur sehr wenig Mühe. Wir reichen einfach die entsprechenden Funktionen an den Container weiter. Wir bieten begin() und end() jeweils in einer konstanten und einer nicht-konstanten Variante an und zusätzlich die Funktionen cbegin() und cend(), welche äquivalent zu den konstanten Versionen sind. Diese beiden Funktionen sind auch Teil des kommenden C++-Standards. Wir werden sie jedoch die aktuellen Container-Methoden begin() und end() aufrufen lassen, was mit dem jetzigen und dem kommenden Standard kompatibel ist. Im Gegensatz zu den Standard-Iteratorfunktionen werden unsere Funktionen alle samt konstante Objekte zurückgeben. Leider ist dies bei den Standardfunktionen nicht so, weshalb man beispielsweise begin()++ schreiben kann, was, wenn Sie kurz darüber nachdenken, vollkommen sinnfrei ist. Unser Iteratorinterface sieht somit so aus:

Nuvola-inspired-terminal.svg
1 public:
2     iterator const       begin()       { return data_.begin(); }
3     const_iterator const begin()const  { return data_.begin(); }
4     iterator const       end()         { return data_.end(); }
5     const_iterator const end()const    { return data_.end(); }
6     const_iterator const cbegin()const { return begin(); }
7     const_iterator const cend()const   { return end(); }

Kapazität[Bearbeiten]

Unter dieser Überschrift wollen wir die Methoden zum Abfragen der Dimension einführen. Die Methode dimension() soll uns das Dimensionsobjekt liefern, zusätzlich werden wir aber noch die Methoden rows() und columns() anlegen. In der Praxis kommt es in etwa gleich häufig vor, dass die komplette Dimension benötigt wird und dass nur ein Teil der Dimension benötigt wird. Keine der beiden Varianten lässt sich effizient durch die jeweils andere ersetzen, daher bieten wir beide an. Außerdem erhöhen wir die Lesbarkeit von Clientcode, indem wir erlauben, ihn kürzer zu fassen, ohne dass dabei Information für den Leser verloren geht. Sie können ja mal versuchen, nur mit der dimension()-Methode oder nur mit den Methoden rows() und columns(), Code zu schreiben, in dem die jeweils andere Schnittstelle der matrix-Klasse günstiger wäre. Die Methoden sehen folgendermaßen aus:

Nuvola-inspired-terminal.svg
1 public:
2     dimension_type const dimension()const { return dimension_; }
3     size_type const      rows()const      { return dimension_.rows(); }
4     size_type const      columns()const   { return dimension_.columns(); }

Dimension ändern[Bearbeiten]

Es gibt eine reichliche Menge an Situationen, in denen es sinnvoll ist, die Größe einer Matrix im Nachhinein zu verändern. Dabei muss man zwei Fälle unterscheiden. Im Ersten werden die Elemente der Matrix komplett verworfen und neu initialisiert. Im zweiten Fall wird nur die Dimension geändert, die Elemente, die auch mit der neuen Dimension noch existieren, bleiben erhalten. Betrachten wir beide Fälle etwas genauer.

Fall Eins lässt sich noch einmal nach der Art der Neuinitialisierung der Elemente aufteilen. Wie schon bei den Konstruktoren, kann auch hier wieder ein Element als Vorlage für alle anderen übergeben werden, oder es kann ein Container mit passender Größe, gegen den aktuellen Elemente-Container ausgetauscht werden. Diese zweite Variante werden wir in einer Methode namens reinit() implementieren. Sie besitzt die gleichen Nachteile wie der entsprechende Konstruktor, bietet aber auch die gleiche, sehr hohe, Effizienz.

Wenn die Elemente an ihren Positionen erhalten bleiben sollen, stellt sich natürlich die Frage, was zu tun ist, wenn die neue Matrix Elemente enthält, die in der alten nicht vorhanden waren. Wir werden das Problem dahingehend lösen, dass wir entsprechende Elemente mit einem übergeben Element auffüllen. Dieses Übergebene Element soll selbstverständlich als Standardparameter das standardkonstruierte Element verwenden, so dass der Benutzer nicht zwingend etwas angeben muss. Speziell wenn er weiß, das er die Matrix verkleinert, wäre es ziemlich nervig, einen Parameter angeben zu müssen, der niemals genutzt wird. Somit wird dies der letzte Parameter unserer resize()-Methode. Die Entscheidung bestehende Elemente zu erhalten, werden wir Standardmäßig auf true setzen. Wenn der Nutzer dies nicht wünscht, muss er es explizit abschalten, andernfalls braucht er sich keine Gedanken darüber zu machen. Unsere Implementierung sieht so aus:

Nuvola-inspired-terminal.svg
 1 public:
 2     void resize(dimension_type const& size,
 3         bool preserve = true, const_reference fill = value_type()
 4     ){
 5         container_type init(elements(size), fill); // Neuer Container
 6 
 7         if(preserve){
 8             // Länge der pro Zeile zu kopierenden Elemente ermitteln
 9             size_type min_columns = std::min(size.columns(), dimension_.columns());
10             // Differenz von kurzen und langen Zeilen
11             size_type dif_columns = std::max(size.columns(), dimension_.columns()) - min_columns;
12             // Iteratoren beider Container
13             iterator  data_iter = data_.begin();
14             iterator  init_iter = init.begin();
15             // Iterator mit den längeren Zeilen ermitteln
16             iterator& bigger    = size.columns() < dimension_.columns() ? data_iter : init_iter;
17             // Zeilenweise durchlaufen
18             for(size_type i = size_type(); i < size.rows() && i < dimension_.rows(); ++i){
19                 // Iteratoren hinter das Ende des zu kopierenden Bereiches setzen
20                 iterator begin_data_iter = data_iter;
21                 iterator begin_init_iter = init_iter;
22                 std::advance(data_iter, min_columns);
23                 std::advance(init_iter, min_columns);
24                 // Kopie durchführen
25                 std::copy(begin_data_iter, data_iter, begin_init_iter);
26                 // Iterator mit den längeren Zeilen auf den Anfang der nächsten Zeile setzen
27                 std::advance(bigger, dif_columns);
28             }
29         }
30 
31         using std::swap;
32         swap(data_, init);
33         dimension_ = size;
34     }
35 
36     void resize(size_type const& rows, size_type const& columns,
37         bool preserve = true, const_reference fill = value_type()
38     ){
39         resize(matrix_size_type(rows, columns), preserve, fill);
40     }
41 
42 
43     void reinit(dimension_type const& size, container_type& data){
44         raw_data_swap(size, data);
45         dimension_ = size;
46     }
47 
48     void reinit(size_type const& rows, size_type const& columns, container_type& data){
49         reinit(matrix_size_type(rows, columns), data);
50     }

Wie Sie erkennen, stellen wir auch für diese Methoden wieder je eine Version mit einem dimension-Objekt und eine mit den Einzelangaben für die Anzahl der Zeilen und Spalten bereit. Die Implementierung erfolgt natürlich nur in einer Version, da am Ende ohnehin ein dimension-Objekt benötigt wird, wird diese Version durch die andere aufgerufen. Bei der Reinitiallisierung verwenden wir wieder die Hilfsmethode raw_data_swap(), welche wir mit den Konstruktoren eingeführt haben. Da diese eine Ausnahme werfen kann, erfolgt dieser Aufruf als erstes. Somit steigt die Wahrscheinlichkeit, dass das matrix-Objekt unverändert bleibt, falls eine Ausnahme geworfen wird. Eine Garantie hierfür, kann man natürlich erst geben, wenn der Containertyp bekannt ist.

In der resize()-Methode legen wir zunächst einen neuen Container mit passender Größe an und lassen ihn mit unserem Füllelement auffüllen. Dies stellt an den Container die Anforderung, dass dieser einen entsprechenden Konstruktor anbietet. Falls preserve gesetzt ist, werden anschließend zeilenweise die Elemente aus dem alten Container kopiert, die für beide Dimensionen existieren. Die übrigen Elemente sind ja bereits auf den Füllwert gesetzt. An dieser Stelle setzten wir voraus, dass der Container Iteratoren anbietet. Da wir dies jedoch bereits in der Klassendefinition verlangt haben, muss dies immer der Fall sein. Schließlich tauschen wir den alten Container gegen den Neuen aus und weisen die neue Dimension zu.

Elementzugriff[Bearbeiten]

Für den Zugriff auf Zeilen und Spalten werden wir sogenannte Proxyklassen verwenden. Der Nutzer unserer Matrixklasse muss von diesen Proxyklassen nicht unbedingt etwas mitbekommen. Was wir erreichen wollen, ist, dass der Zugriff auf ein Element innerhalb der Matrix syntaktisch mit matrix_object[3][2] oder auch mittels matrix_object.column(2).row(3) möglich ist. Dies erreichen wir, indem wir die Memberfunktion von Matrix (operator[]() bzw. column()) ein Objekt zurückgeben lassen, welches von einem Typ ist, der wiederum die zweite Memberfunktion (operator[]() bzw. row()) bereitstellt. Diesen „Zwischentyp“ bezeichnet man als Proxyklasse. Wie genau diese Proxyklasse heißt oder aussieht, ist für den Nutzer erst interessant, wenn er eine Zeile oder Spalte als Parameter an eine eigene Funktion übergeben möchte. Solange er nur auf Elemente zugreift, muss er lediglich wissen, wie der Aufruf syntaktisch aussieht.

Wir werden die Proxyklassen in einem eigenen Kapitel behandeln. An dieser Stelle werden wir einfach annehmen, sie würden bereits existieren. Unsere Zugriffsmethoden der matrix-Klasse werden also ein Objekt vom Typ einer Proxyklasse erstellen, ihm die Information mitteilen, die sie selbst erhalten haben (welche Zeile / Spalte) und das Objekt schließlich zurückgeben. Zu beachten ist, dass es für konstante und nicht-konstante Versionen der Zugriffsfunktionen getrennte Proxyklassen gibt. Dies ist äquivalent zu den Funktionen für Iteratoren, denn genau wie die Iteratoren, müssen ja auch die Proxy-Objekte wissen, ob Sie auf eine konstante oder auf eine nicht-konstante Matrix verweisen. Ob die Iteratoren bzw. Proxys selbst konstant oder nicht-konstant sind, gibt keinen Aufschluss über die Daten, auf die sie verweisen. Die zurückgegeben Objekte sind natürlich, wie schon bei den Iteratoren, wieder konstant. Somit erhalten wir folgende Zugriffsmethoden für matrix:

Nuvola-inspired-terminal.svg
 1 public:
 2     row_proxy const       operator[](size_type const& row_number)      { return row(row_number); }
 3     row_const_proxy const operator[](size_type const& row_number)const { return row(row_number); }
 4 
 5     row_proxy const row(size_type const& number){
 6         return row_proxy(*this, number);
 7     }
 8 
 9     row_const_proxy const row(size_type const& number)const{
10         return row_const_proxy(*this, number);
11     }
12 
13     column_proxy const column(size_type const& number){
14         return column_proxy(*this, number);
15     }
16 
17     column_const_proxy const column(size_type const& number)const {
18         return column_const_proxy(*this, number);
19     }

Modifizierer[Bearbeiten]

Als Modifizierer bezeichnen wir Methoden, welche Zeilen oder Spalten (nachfolgend mit Linien abgekürzt) entfernen oder hinzufügen. Das Hinzufügen von Linien lassen wir in mitrax außen vor. Da es jedoch einige Algorithmen gibt, welche einzelne Linien löschen, werden wir diese Möglichkeit anbieten. Als Parameter bekommen unsere beiden Methoden, eine Position und die Anzahl der Linien, welche nach dieser Position gelöscht werden sollen. Das mehrere Zeilen oder Spalten gelöscht werden, kommt zwar relativ selten vor, aber wenn es doch mal nötig ist, wäre es sehr ineffizient die beiden aufeinander folgenden Linien einzeln zu löschen. Außerdem macht es uns kaum Mehrarbeit und als Standardwert werden wir für den zweiten Parameter natürlich eine Eins angeben.

Vor der eigentlichen Löschung prüfen wir, ob die entsprechenden Linien auch existieren. Falls dies nicht der Fall ist, werfen wir eine entsprechende Ausnahme. Sie können zum Testen eine std::out_of_range-Ausnahme werfen. Anschließend löschen wir die entsprechenden Elemente in unserem Daten-Container. Zum Schluss setzen wir die Dimension auf ihren neuen Wert. Das ganze sieht folgendermaßen aus:

Nuvola-inspired-terminal.svg
 1 public:
 2     void erase_row(size_type const& number, size_type const& count = size_type(1)){
 3         if(rows() > number + count){
 4             throw error::row_access("mitrax::matrix<>::erase_row", dimension_, rows());
 5         }
 6 
 7         // Zeilen löschen
 8         iterator iter = data_.begin();
 9         iter += number * columns();
10         data_.erase(iter, iter + count * columns());
11         // Dimension anpassen
12         dimension_.resize(rows() - count, columns());
13     }
14 
15     void erase_column(size_type const& number, size_type const& count = size_type(1)){
16         if(columns() < number + count){
17             throw error::column_access("mitrax::matrix<>::erase_column", dimension_, columns());
18         }
19 
20         // Zeilen löschen
21         for(size_type i = size_type(); i < rows(); ++i){
22             iterator iter = data_.begin();
23             iter += number + (columns() - count) * i;
24             data_.erase(iter, iter + count);
25         }
26         // Dimension anpassen
27         dimension_.resize(rows(), columns() - count);
28     }

Wir wollen nachfolgend einige theoretische Betrachtungen zu den Anforderungen an size_type in diesen beiden Methoden vornehmen. Praktisch werden diese höchstwahrscheinlich immer erfüllt sein, den der size_type eines Container ist so gut wie immer ein ganzzahliger eingebauter Datentyp.

Falls der Standardwert für den zweiten Parameter genutzt wird, muss der Integerliteral 1 in ein size_type-Objekt konvertierbar sein. Wäre nicht explizit eine Erstellung eines size_type-Objekts angegeben, müsste diese Umwandlung zusätzlich implizit möglich sein. Die beiden Methoden verlangen, dass size_type standardkonstruierbar ist. Außerdem müssen die Operatoren operator<(), operator++(), operator+(), operator-() und operator*() für size_type überladen sein. Bei der üblichen Implementierung der letzten drei Operatoren folgt, dass size_type kopierkonstruierbar sein muss. Wegen den Vorschriften die C++ zu Random-Access-Iteratoren macht, muss size_type implizit in difference_type konvertierbar sein. Der zweite Parameter des operator+=() eines Random-Access-Iterators muss nämlich von diesem Typ sein.

swap[Bearbeiten]

Wie zuvor schon erwähnt wurde, ist swap() für Container normalerweise durch eine sehr effiziente Variante überladen. Wenn Sie sich nun an unsere Überlegungen zur swap-Funktion von dimension erinnern, werden Sie dieses mal zu dem Schluss kommen, dass sich die Überladung für matrix lohnt. Wir werden einfach für alle Datenmember von matrix swap() aufrufen. Der einzige Nachteil, der sich theoretisch daraus ergibt, besteht darin, dass die swap()-Funktion ihre beiden Operanden in einem undefinierten Zustand belässt, falls bei einem der Unteraufrufe eine Ausnahme geworfen wird. Dies ist jedoch ausgesprochen unwahrscheinlich. Das swap() für die dimension-Objekte kann keine Ausnahme werfen. Dass das swap() für die Container eine Ausnahme wirft, ist ebenfalls unwahrscheinlich, denn wie bereits erläutert haben, werden hier typischerweise nur zwei Zeiger vertauscht. Da das Container-swap() noch einen Hauch kritischer ist, werden wir diesen Aufruf zuerst tätigen. Falls hier tatsächlich eine Ausnahme geworfen werden sollte, haben wir dimension noch nicht verändert und somit ist alles in Ordnung. Die Implementierung sieht wie Folgt aus:

Nuvola-inspired-terminal.svg
 1 public:
 2     void swap(matrix& m){
 3         using std::swap;
 4         swap(data_, m.data_);
 5         swap(dimension_, m.dimension_);
 6     }
 7 
 8 // Außerhalb der Klasse
 9 template < typename T, typename Container >
10 void swap(matrix< T, Container >& lhs, matrix< T, Container >& rhs){
11     lhs.swap(rhs);
12 }

Helferlein[Bearbeiten]

Wie bereits angedeutet, werden wir noch eine kleine Helferklasse einführen, die es den Nutzer ermöglicht, auf einfache Weise die Position in einem eindimensionalen Feld anhand einer zweidimensionalen Angabe zu berechnen. Wir erstellen zu diesem Zweck einen Funktor, also eine Klasse, die den Funktionsoperator überlädt. Im Konstruktor teilen wir dem Funktor mit, wie viele Spalten unsere Matrix letztlich haben soll. Dem Funktionsoperator übergeben wir die X- und die Y-Position. Das Ergebnis wird die eindimensionale Position innerhalb des Containers sein.

Der Datentyp der Positionen ist von der verwendeten matrix-Instanz anhängig. Da es jedoch sehr viele matrix-Instanzen gibt, die den gleichen size_type verwenden, ist es sinnvoll, wenn wir nur eine Templateinstanz unseres Funktors pro size_type anlegen. Daher werden wir die Klasse als Template mit einem Parameter anlegen. Den Zugriff auf die passende Funktorklasse realisieren wir, indem wir innerhalb unserer matrix-Klasse einen typedef für die passende Funktorklasse hinzufügen. Da der Nutzer folglich keine Kenntnis von der Definition der Funktorklasse haben muss, werden wir diese im Namensraum detail innerhalb von mitrax deklarieren.

Diesen Namensraum werden wir innerhalb des Projekts für alles verwenden, mit dem der Nutzer niemals direkten Kontakt haben sollte. Wir werden den Nutzer lediglich innerhalb der Dokumentation über die Verwendung bei indirekter Nutzung aufklären. Das hat für uns den Vorteil, dass wir uns keine größeren Gedanken über den Teil der Schnittstelle machen müssen, den der Nutzer sowieso nicht sieht. Insbesondere können wir auf diese Weise, bei einer späteren Version unserer Bibliothek, problemlos den Teil der Schnittstellen ändern, mit denen der Clientcode keinen Kontakt hat. In Zusammenhang mit den Iteratoren für die Proxyklassen werden Sie dies leicht anhand der Konstruktoren erkennen. Wir werden unsere Funktorklasse auf den Namen position_adapter taufen. Der typedef wird den gleichen Namen tragen.

Nuvola-inspired-terminal.svg
 1 namespace detail{
 2 
 3 template < typename SizeType >
 4 class position_adapter{
 5 public:
 6     position_adapter(SizeType columns):columns_(columns){}
 7     position_adapter(dimension< SizeType > const& dim):columns_(dim.columns()){}
 8 
 9     SizeType const operator()(SizeType const& row, SizeType const& column){
10         return row * columns_ + column;
11     }
12 
13 private:
14     SizeType columns_;
15 };
16 
17 }
18 
19 template < typename T, typename Container = std::vector< T > >
20 class matrix{
21 public:
22     // andere typdefs
23     typedef detail::position_adapter< size_type > position_adapter;
24     // ...
25 };

Der Nutzer kann nun zur effizienten Initialisierung einer Matrix beispielsweise folgendes schreiben, ohne sich Gedanken über die Berechnung der eindimensionalen Position machen zu müssen.

Nuvola-inspired-terminal.svg
 1 typedef mitrax::matrix< int > matrix;
 2 matrix::dimension_type dimension(3, 5);
 3 matrix::container_type init(elements(dimension));
 4 matrix::position_adapter index(dimension);
 5 
 6 for(matrix::size_type row = 0; row < dimension.rows(); ++row)
 7     for(matrix::size_type column = 0; column < dimension.columns(); ++column)
 8         init[index(row, column)] = row;
 9 
10 matrix a1(dimension, init);