Zum Inhalt springen

C++-Programmierung/ Eine Matrix-Bibliothek – mitrax/ Proxyklassen

Aus Wikibooks


In diesem Kapitel wollen wir dafür sorgen, dass der Zugriff auf einzelne Elemente unserer Matrix durch zweimalige Anwendung des Indexoperators erreicht werden kann. Zu diesem Zweck schreiben wir eine Reihe von Klassen, die eine Referenz auf eine Zeile oder Spalte, eines matrix-Objektes, repräsentieren. Zunächst müssen wir uns Gedanken darüber machen, welche dieser Proxyklassen wir benötigen und wie diese miteinander in Verbindung stehen. Die Inhalte dieses Kapitels stehen in der Datei proxy.hpp.

Vorüberlegungen

[Bearbeiten]
Einfache Vererbungshierarchie der Proxyklassen

Wie Sie bereits im vorherigen Kapitel erfahren haben, werden für konstante und nicht-konstante Zeilen- bzw. Spaltenproxys jeweils getrennte Klassen benötigt. Somit benötigen wir also vier Proxyklassen. Die Klassen für nicht-konstanten Matrizen sollten an jeder Stelle verwendet werden können, an denen ihre Äquivalente für konstanten Matrizen gefordert werden. Dies können wir scheinbar auf zweierlei Arten erreichen. Wir können die Klassen für nicht-konstanten Matrizen öffentlich von den Klassen für konstanten Matrizen ableiten, oder wir können den Klassen für konstante Matrizen jeweils einen Konstruktor verpassen, der ein Objekt aus einem Objekt der entsprechenden Proxyklasse für nicht-konstante Matrizen erstellt.

Im Prinzip würden diese vier Proxyklassen genügen, wir wollen jedoch noch etwas weiter denken. An vielen Stellen können sowohl Spalten-, als auch Zeilenproxys verwendet werden. Es ist also zu überlegen, wie wir eine solche richtungsunabhängige Verarbeitung realisieren können. Zunächst muss natürlich für beide Proxyarten eine einheitliche Schnittstelle existieren. Dann kann die gewünschte Funktion als Template erstellt werden, welches die konkrete Proxyklasse als Parameter bekommt. Diese sehr einfache Herangehensweise ist zugleich auch sehr effizient, sie weist jedoch auch ein paar Nachteile auf.

Falls innerhalb der Funktion nur wenig mit dem Proxyobjekt gearbeitet wird, entsteht eine Menge doppelter Code bei der Templateinstantiierung. Ein guter Compiler wird dies natürlich optimieren, daher wiegt diese Nachteil wahrscheinlich nicht alt so schwer. Ein anderer Nachteil besteht darin, dass der Nutzer richtungsunabhängige Funktionen immer als Templates realisieren muss. Da es nur zwei Arten von Proxys geben kann, wäre es natürlich angenehm, wenn er einfach eine allgemeine Proxyklasse als Parameter angeben könnte. Besonders problematisch wird es, wenn seine Funktion eine Proxyklasse für konstante Matrizen erwartet. Der Compiler wird, selbst bei einer Verwandtschaftsbeziehung zwischen den Proxys für konstante und nicht-konstante Matrizen, vier Instantiierungen des Funktionstemplats vornehmen, anstatt lediglich zwei für die jeweils auf konstante Matrizen bezogenen Proxyklassen.

Richtungsunabhängige Proxyklassen

[Bearbeiten]
Vererbungshierarchie mit richtungsunabhängigen Proxyklassen

Wir werden dieses Problem später genauer beleuchten, da zunächst klar sein muss, was die vier, zwingend benötigten, Proxyklassen anbieten müssen.

Wie Sie am Beispiel der element_swap()-Funktion noch sehen werden, sind bei der Nutzung von richtungsabhängigen Proxys leider zwei Funktionsdeklarationen notwendig, auch wenn das eigentliche Problem richtungsunabhängig ist. Es wäre natürlich praktisch, wenn wir, über eine entsprechende Vererbungshierarchie, nur noch eine Deklaration angeben müssten. Das Ganze könnte wie in der Grafik dargestellt aussehen. Dabei darf es nicht möglich sein, von den Klassen line_const_proxy und line_proxy Objekte zu erzeugten. Folglich muss line_proxy nicht von line_const_proxy abgeleitet sein, denn alle Klassen die von line_proxy abgeleitet sind, können ja zusätzlich auch indirekt von line_const_proxy abgeleitet werden.

Diese Tatsache ist insbesondere deshalb von Vorteil, weil eine zusätzliche Vererbung von line_const_proxy an line_proxy virtuell sein müsste und virtuelle Vererbung bringt immer Laufzeitnachteile mit sich. Wenn wir nun die Daten aus den richtungsunabhängigen Proxyklassen für konstante Matrizen nach line_const_proxy verschieben, funktioniert dies erst einmal sehr schön. Leider haben wir von line_proxy aus keine Möglichkeit an die Daten heranzukommen. Wir können uns eine solche Möglichkeit auf zweierlei Weise verschaffen.

Die Erste besteht darin, abstrakte virtuelle Funktionen zu verwenden, welche im privaten Bereich der richtungsabhängigen Klassen implementiert werden. Die Folge virtueller Funktionen ist jedoch unweigerlich eine wesentlich schlechtere Laufzeitperformance. Die andere Möglichkeit ist, line_proxy eben doch von line_const_proxy abzuleiten. Wenn diese Ableitung nicht virtuell geschieht, existiert die Basisklasse line_const_proxy und mit ihr auch die Datenmember, mehrfach. Wenn die Ableitung virtuell ist, ergeben sich, wie schon erwähnt, ebenfalls beträchtliche Laufzeiteinbußen und das sogar, wenn wir einen richtungsabhängigen Proxy verwenden. Daher kommt dies nicht in Frage. Wir stellen also fest, dass es keine effiziente Möglichkeit für eine derartige Implementierung gibt.

Bleibt noch die Frage zu klären, ob die Einbußen klein genug sind, um sie für eine einfachere Verwendung in Kauf nehmen zu können. Der Elementzugriff über die Proxyklassen, sollte, dank der Compileroptimierung für inline-Methoden, ebenso schnell sein, wie ein Zugriff, der direkt über eine Zugriffsmethode eines matrix-Objektes durchgeführt wurde. Wenn eine Zugriffsmethode virtuelle Funktionen aufruft, ist zumindest für diese kein inlining mehr möglich. Folglich haben wir hier eine recht drastische Einbuße, für einen schlichten Zugriff auf ein Element innerhalb eines Array. Noch schlimmer sieht die Situation aus, wenn wir mit Iteratoren arbeiten wollten, denn auch diese müssten dann in irgendeiner Form generisch sein. Wir fangen uns mindestens eine komplizierte Berechnungsmethodik ein, weil wir ja sowohl das Verhalten von Zeilen-, als auch das Verhalten von Spalteniteratoren unterstützen müssten. Außerdem währen die Iteratoren für Zeilen und Spalten zwingend vom gleichen Typ, sobald wir die richtungsunabhängige Proxyklasse verwenden. Das heißt, wir können unsinniger Weise die Vergleichsoperatoren anwenden, ohne dass der Compiler sich darüber beschwert.

Alles in allem, sind die Nachteile also überwältigend. Daher werden wir auf ein solches Konstrukt verzichten und dem Nutzer stattdessen zumuten, sich mit einer ähnlichen Technik zu behelfen, wie wir sie bei element_swap() verwenden werden. Effektiv wird er dabei glücklicherweise oft nur zwei Deklarationen schreiben müssen, denn falls er Proxys für konstante Matrizen benötigt, sind die Objekte von Proxys für nicht-konstante Matrizen kompatibel und wenn er Proxys für nicht-konstante Matrizen benötigt, können die anderen beiden Proxys ohnehin nicht verwendet werden. Nur wenn er Proxys für konstante Matrizen benötigt und die Funktion ein Template ist, kann keine implizite Typumwandlung in Proxys für nicht-konstante Matrizen stattfinden. Ein Beispiel dazu werden Sie bei der Ausgabe von Proxys sehen. Leider tritt dieser eine Fall recht häufig ein, aber eine einfacheren Weg gibt es hier nicht.

Vererbung oder implizite Typumwandlung

[Bearbeiten]

Wir haben oben schon angedeutet, dass es zwei Varianten gibt, um eine Kompatibilität der Proxys für nicht-konstante Matrizen, zu den Proxys für konstante Matrizen zu erzeugen. Die Erste ist eine öffentlich Ableitung, die Zweite ist ein Konstruktor innerhalb der Proxys für konstante Matrizen, der ein Objekt eines Proxys für nicht-konstante Matrizen übernimmt. Wir wollen uns die Unterschiede dieser beiden Techniken kurz ansehen, um festzustellen, welche Technik besser für unser Problem geeignet ist. Nehmen Sie an, sie besäßen die folgenden Funktionen und würden ihnen je ein Objekt vom Typ proxy übergeben.

void f1(proxy_const value);
void f2(proxy_const const& value);
void f3(proxy_const& value);

Nehmen wir zunächst an, proxy wäre öffentlich von const_proxy abgeleitet. Bei Aufruf der ersten Funktion würde unser Objekt implizit, mittels des Kopierkonstruktors von const_proxy, umgewandelt werden. Das ist für uns in Ordnung, denn innerhalb der Funktion wird ja auch nur der Basisklassenanteil benötigt. Die zweite Funktion würde das Objekt als Referenz übernehmen und uns lediglich den Zugriff auf den Basisklassenanteil gestatten. Da dieser konstant ist, haben wir keine ungewollten Änderungen zu befürchten. Ganz anders sieht es jedoch bei der Funktion f3() aus. Hier wird ebenfalls eine Referenz übernommen, jedoch lässt sich das Objekt dahinter verändern und zwar so, als wäre es tatsächlich ein proxy_const Objekt. Das bedeutet insbesondere, das wir einem solchen Objekt mittels Kopierzuweisung ein anderes Objekt vom Typ proxy_const zuweisen könnten. Wir wollen das am Beispiel der Zeilenproxys kurz ausprobieren, wobei wir allerdings auf die Nutzung einer Funktion verzichten.

matrix       dummy_matrix(2, 2); // Veränderliche Matrix (Inhalt egal)
matrix const const_matrix(2, 2); // Unveränderliche Matrix

matrix::row_const_proxy  const_proxy = const_matrix.row(0);
// Initialisierung nötig weil kein Standardkonstruktor existiert
matrix::row_proxy        dummy_proxy = dummy_matrix.row(0);

// Mit veränderlicher Matrix initialisieren
matrix::row_const_proxy& bad_proxy = dummy_proxy; 
// Gewünschten const_proxy dem Objekt hinter der Referenz zuweisen
bad_proxy = const_proxy;

// Veränderlicher Proxy bezieht sich jetzt auf eine Zeile in der konstanten Matrix
dummy_proxy[0] = 100;

Wie Sie sehen ist keinerlei const_cast notwendig, um einen Schreibzugriff auf die konstante Matrix zu bekommen. Das Ergebnis der Schreiboperation ist natürlich entweder undefiniert oder hängt zumindest von Stelle ab, an der const_matrix definiert wurde. In jedem Fall darf solcher Code nicht funktionieren, daher scheidet die öffentliche Vererbung für unsere Zwecke aus. Dieses Beispiel ist übrigens gar nicht so konstruiert, wie es auf den ersten Blick aussieht. Wenn Sie die gewünschte Zuweisung innerhalb einer Funktion ausführen, welche, wie in f3(), eine Referenz übernimmt, dann sieht es viel weniger konstruiert aus und fällt auch auf den ersten Blick viel weniger auf. So etwas kann also durchaus versehentlich geschehen.

Schauen wir uns noch an, ob bei der Verwendung einer Umwandlung mittels Konstruktor eine ähnlich böse Falle auf uns lauert. Für den Aufruf von f1() würde der Compiler wieder eine implizite Typumwandlung durchführen. Der entsprechende Konstruktor darf dabei natürlich nicht explizit sein. Auch die zweite Funktion würde eine implizite Typumwandlung nach sich ziehen. Das ist natürlich geringfügig langsamer, als bei Variante mit Vererbung, aber dagegen kann uns, mit etwas Glück, die Optimierung durch den Compiler helfen. Der Aufruf der Funktion f3() führt zu einer Fehlermeldung und das ist genau das Verhalten, das wir benötigen. Wenn eine solche Funktion aufgerufen werden soll, muss der Nutzer zuvor explizit ein Objekt vom Typ const_proxy aus seinem proxy-Objekt erzeugen und dieses übergeben.

Dieses Verhalten lässt sich übrigens auch bei Iteratoren beobachten. Es wurde ja schon mehrfach darauf hingewiesen, das Proxyklassen und Iteratorklassen sich ähnlich verhalten. In beiden Fällen repräsentieren die Objekte Daten aus einer anderen Klasse. Im Wesentlichen handelt es sich also um den gleichen Typ von Technik. Denken Sie, bei der Erstellung solcher proxyartiger Objekte, daher immer daran, dass eine öffentlich Vererbung zu unschönen Problemen führen kann. Das Mittel der Wahl heißt implizite Typumwandlung. Dass kann zwar geringfügig langsamer sein, aber dafür ist die Technik sicher und wenn ihr Compiler gut optimiert, dann ist selbst die Übergabe als Referenz auf const, genau so schnell, wie bei der Vererbung.

Proxys für konstante Matrizen

[Bearbeiten]

Als Templateparameter bekommen unsere Proxyklassen selbstverständlich die konkrete matrix-Klasse. Für die Namen werden wir uns, wie im Bild oben schon angedeutet, an den Konventionen für die Iteratornamen halten. Diesen stellen wir noch die Richtung, also „row“ oder „column“, voran. Somit ergeben sich die vier Namen, die im ersten Bild zur Vererbungshierarchie verwendet wurden.

Als nächstes sollten wir uns wieder fragen, welche typedefs wir benötigen. Praktischerweise sollten wir natürlich den Templateparameter wieder verfügbar machen und um den Zugriff zu erleichtern, werden wir auch den value_type von matrix wieder unter dem gleichen Namen zur Verfügung stellen. Die Elemente der Proxyklassen sind schließlich vom gleichen Typ, wie jene, der entsprechenden matrix. Da die Proxyklassen eine bestimmte Anzahl von Elemente repräsentieren, benötigen wir natürlich einen size_type. Dieser sollte ebenfalls mit jenem, aus der zugehörigen matrix-Klasseninstanz übereinstimmen.

template < typename Matrix >
class row_const_proxy{
public:
    typedef Matrix                      matrix_type;
    typedef typename Matrix::value_type value_type;
    typedef typename Matrix::size_type  size_type;

    // ...
};

template < typename Matrix >
class column_const_proxy{
public:
    typedef Matrix                      matrix_type;
    typedef typename Matrix::value_type value_type;
    typedef typename Matrix::size_type  size_type;

    // ...
};

Schließlich wurde ja bereits gesagt, dass wir auch für die Proxyklassen ein Iteratorinterface anbieten wollen. Diesmal können wir leider nicht einfach die Iteratoren eines Datenmembers durchreichen. Wir müssen die Iteratortypen selbst schreiben und da dies ein etwas aufwendigeres Unterfangen ist, werden wir das in einem eigenen Kapitel behandeln. Die drei typedefs, die wird für die Iteratoren benötigen, sind iterator, const_iterator und difference_type. Bei den Proxys für konstante Matrizen verweisen die typedefs für iterator und const_iterator sinnvoller Weise auf den gleichen Typ. Da wir jedoch an dieser Stelle nicht unkompliziert vorgreifen können, werden wir den gesamten Teil der Proxyklassen, der Iteratoren betrifft, später zusammen mit den Iteratorklassen selbst Abhandeln.

Schauen wir uns an, welche Konstruktoren wir benötigen könnten. Ein Standardkonstruktor ergibt für die Proxys keinen Sinn. Die Proxyklassen sollen ja eine Zeile oder Spalte eines matrix-Objekts repräsentieren, wie sollten wir sie also ohne ein matrix-Objekt sinnvoll Initialisieren. Einen Kopierkonstruktor könnten wir hingegen durchaus gebrauchen. Es ist gut möglich, dass ein Nutzer von mitrax, in einem Algorithmus lieber zwei Proxys vertauscht, als tatsächlich die komplette Zeile oder Spalte auszutauschen. Da für diesen Zweck üblicherweise swap() verwendet wird und wir keine spezielle Überladung dieser Funktion zur Verfügung stellen werden, ergibt sich, dass neben dem Kopierkonstruktor auch die Kopierzuweisung existieren sollte. In beiden Fällen werden wir jedoch die compilergenerierten Versionen verwenden, so dass wir hierfür nichts tun müssen.

Um sicherzustellen, dass dies auch möglich ist, müssen wir uns überlegen, welche Datenmember unsere Proxyklassen besitzen müssen und welchen Typ diese haben. Selbstverständlich, muss ein Verweis auf ein matrix-Objekt existieren. Da wir eine Kopierzuweisung erlauben wollen, darf dieser nicht als Referenz implementiert sein. Weiterhin sollen die beiden Proxyklassen, um die wir uns momentan kümmern, einen Verweis auf ein konstantes matrix-Objekt repräsentieren. Der Datentyp für diesen Member muss daher matrix const* lauten. Der Variablenname wird matrix_ lauten. Außerdem benötigen wir natürlich die Zeile oder Spalte, die unser jeweiliges Proxyobjekt repräsentieren soll. Dieser Member muss vom Typ size_type sein und wird pos_ heißen. Alle weiteren, eventuell benötigten Informationen, sind im referenzierten matrix-Objekt enthalten. Entsprechend können wir uns diese, über den Zeiger auf das matrix-Objekt, beschaffen.

Somit kann der Compiler die beiden gewünschten Methoden erstellen und uns bleibt noch, den eigentlichen Konstruktor zur Objekterzeugung zu realisieren. Er muss die beiden Member initialisieren, die wir soeben eingeführt haben. Da für das matrix-Objekt immer ein gültiger Verweis benötigt wird, ist es an dieser Stelle sinnvoll, den Konstruktorparameter als matrix const& zu implementieren. Somit vermeiden wir, dass ein Nullzeiger angegeben werden kann. Bei der Initialisierung des Datenmembers lassen wir uns dann einfach die Adresse geben. Der zweite Parameter sollte vom Typ size_type const& sein. Da size_type typischerweise ein eingebauter Datentyp ist, könnte eine Übergabe mittels „Call by value“ zwar effektiver sein, aber die Compileroptimierung sollte in der Lage sein, dies gegebenenfalls selbst zu erkennen und entsprechend umzusetzen. Mit der Übergabe als Referenz auf const machen wir daher nichts falsch.

// Vor den Proxyklassen - Forward-Deklarationen
template < typename Matrix > class row_proxy;
template < typename Matrix > class column_proxy;

public: // In row_const_proxy
    row_const_proxy(matrix_type const& matrix, size_type const& row):
        matrix_(&matrix), pos_(row){}

public: // In column_const_proxy
    column_const_proxy(matrix_type const& matrix, size_type const& column):
        matrix_(&matrix), pos_(column){}

An Funktionalität benötigen wir den Elementzugriff und die Abfrage, wie viele Elemente die aktuelle Zeile bzw. Spalte besitzt. Außerdem sollte der Nutzer in der Lage sein, abzufragen, auf die wievielte Zeile bzw. Spalte einer Matrix ein Proxyobjekt verweist.

Den Elementzugriff ermöglichen wir zum einen natürlich über den Indexoperator. Zusätzlich werden wir aber in row_const_proxy die Methode column() und in column_const_proxy die Methode row() mit identischer Funktionalität anbieten. Auf diese Weise kann der Nutzer an den Stellen, die Übersichtlichkeit seines Quellcodes erhöhen, an denen es wesentlich und gleichzeitig nicht offensichtlich ist, ob auf ein Elemente innerhalb einer Zeilen oder innerhalb einer Spalte zugegriffen wird.

public: // In row_const_proxy
    value_type const& operator[](size_type const& column_number)const{
        return column(column_number);
    }

    value_type const& column(size_type const& number)const{
        return *(begin() + number);
    }

public: // In column_const_proxy
    value_type const& operator[](size_type const& row_number)const{
        return row(row_number);
    }

    value_type const& row(size_type const& number)const{
        return *(begin() + number);
    }

Beim Elementzugriff wird Ihnen sicher auffallen, dass wir eine Funktion begin() aufrufen, welche jedoch noch nicht existiert. Sie gehört natürlich zum Iteratorinterface des Proxys und wird entsprechend im nächsten Kapitel eingeführt. Die Nutzung der Iteratoren vermeidet, dass wir zweimal über die Positionsberechnung im Container der Matrixklasse nachdenken müssen. Wir vermeiden also Codeverdopplung und erhöhen somit Übersichtlichkeit und Wartbarkeit unseres Quellcodes. Wir lassen die allgemeinen Funktionen jeweils die richtungsspezifischen aufrufen, um noch einmal deutlich zu machen, was passiert.

Ähnlich sieht es bei den Methoden für die Anzahl der Elemente aus. size() wird für beide Proxys eine einheitliche Schnittstelle bieten, während die Methoden columns() bzw. rows() explizit andeuten, ob es sich um die Anzahl der Elemente in einer Zeile oder in einer Spalte handelt. Beachten Sie an dieser Stelle, dass die Methoden mit einem „s“ am Ende, wieder eine Anzahl von Elemente zurückgeben, während sich die Methoden ohne „s“ auf ein konkretes Element beziehen. Die aktuelle Position innerhalb der Matrix werden wir in beiden Proxys über die Methode pos() zurückgeben. Wir könnten die Methode auch position() nennen, aber die Abkürzung „pos“ ist stark verbreitet und wird selbst von unerfahren Programmieren leicht mit „position“ assoziiert, so dass wir getrost die Abkürzung verwenden können.

public: // In row_const_proxy
    size_type const columns()const{ return matrix_const().columns(); }
    size_type const size()const{ return columns(); }

    size_type const pos()const{ return pos_; }

public: // In column_const_proxy
    size_type const rows()const{ return matrix_const().rows(); }
    size_type const size()const{ return rows(); }

    size_type const pos()const{ return pos_; }

Schließlich bleibt noch die Frage zu klären, in welchem Zugriffsbereich unserer Klasse wir die Datenmember unterbringen. Es gibt keinen guten Grund, warum eine abgeleitete Klasse Schreibzugriff auf einen unserer Datenmember haben sollte. Genaugenommen soll auch gar keine Klasse von unseren Proxys abgeleitet werden. Wir werden daher beide Datenmember im private-Bereich unterbringen. Für die Matrix werden wir eine Zugriffsmethode verwenden, so dass wir den Zeiger nicht versehentlich ändern können. Außerdem kann diese gleich die Dereferenzierung unseres Zeigers erledigen. Wir werden die Methode matrix_const() nennen. Sie werden in Kürze erfahren, warum wir nicht einfach matrix() als Namen verwenden.

private: // In row_const_proxy
    matrix_type const& matrix_const()const{ return *matrix_; }

    matrix_type const* matrix_;
    size_type          pos_;

private: // In column_const_proxy
    matrix_type const& matrix_const()const{ return *matrix_; }

    matrix_type const* matrix_;
    size_type          pos_;

Gemeinsamkeiten

[Bearbeiten]

Nun sehen Sie natürlich, dass zwei der Methoden und auch die beiden Datenmember für beide Proxyklassen identisch sind. Es wäre natürlich für die Wartbarkeit von Vorteil, wenn wir die identischen Teile auslagern könnten. In der Tat können wir dies mittels Vererbung realisieren, allerdings dürfen wir dabei nicht öffentlich ableiten. Die Ableitung soll einer „ist implementiert in Form von“-Situation entsprechen und daher leiten wir privat ab. Eine protected-Ableitung wäre ebenfalls möglich, jedoch sollen die Proxyklassen am Ende der Vererbungshierarchie stehen. Daher ist eine private Vererbung besser geeignet.

In der gemeinsamen Basisklasse packen wir die Datenmember in den private-Bereich und die Funktionen, die für beide Proxyklassen identisch sind, in den protected-Bereich. Dies betrifft die Zugriffsmethode matrix_const(), sowie die Methode pos(). Nebenbei erreichen wir hierdurch auch, dass die Datenmember innerhalb der Proxyklassen nicht verändert werden können. Lediglich der Zuweisungsoperator darf dies tun. Allerdings auch nur indirekt, indem er den Zuweisungsoperator der Basisklasse aufruft. Auch in dieser Situation, können wir uns auf die compilergenerierten Versionen verlassen.

Die Basisklasse selbst nennen wir line_const_proxy. Der Benutzer muss über diese Klasse nichts wissen, daher werden wir sie im Namensraum detail platzieren. Da wir nicht möchten, dass direkt Objekte von line_const_proxy erzeugt werden können, werden wir die Konstruktoren ebenfalls in den protected-Bereich packen. Um Kopierkonstruktor und Kopierzuweisung müssen wir uns nicht kümmern, da keine Objekte von line_const_proxy erzeugt werden können und eine Verwendung eines Objekts einer abgeleiteten Klasse als Kopierkonstruktorparameter nur bei öffentliche Vererbung möglich ist. Es wird also nicht schaden, wenn die beiden Funktionen im public-Bereich stehen. Den Templateparameter Matrix geben wir einfach an die Basisklasse weiter. Diese sieht somit folgendermaßen aus:

template < typename Matrix >
class line_const_proxy{
protected:
    typedef Matrix                      matrix_type;
    typedef typename Matrix::value_type value_type;
    typedef typename Matrix::size_type  size_type;

    line_const_proxy(matrix_type const& matrix, size_type const& pos):
        matrix_(&matrix), pos_(pos){}

    size_type const pos()const{return pos_;}

    matrix_type const& matrix_const()const{return *matrix_;}

private:
    matrix_type const* matrix_;
    size_type          pos_;
};

In der Folge müssen wir einige Implementierungen unserer beiden Klassen ändern. Die Konstruktoren reichen ihre Parameter nun einfach hoch. Da die Basisklasse ein Template ist, können wir leider keine using-Deklaration für pos() verwenden. Wir müssen stattdessen die pos-Methode der Basisklasse durch eine neue Definition aufrufen lassen. Überall wo bisher die Methode matrix_const() verwendet wird, werden sie beim Übersetzen nun eine Fehlermeldung kassieren. Der Compiler wird sich darüber beschweren, dass er den Bezeichner matrix_const() nicht kennt. Dies liegt daran, dass die Basisklasse ihrerseits eine Klasseninstanz eines Klassentemplates ist.

Um den Compiler zu überreden, einen Namen auch in solchen Basisklassen zu suchen, haben wir zwei Möglichkeiten. Wir können den Bezeichner über den this-Zeiger ansprechen oder die Basisklasse explizit angeben. Letzteres ist viel Schreibarbeit und oft auch schlecht zu lesen. Außerdem kann es in Zusammenhang mit Polymorphie gelegentlich zu unerwünschten Ergebnissen führen. Darauf wollen wir jedoch an dieser Stelle nicht näher eingehen. Dennoch sollten Sie sich angewöhnen, diese Art des Aufrufs wirklich nur dann zu nutzen, wenn sie auch notwendig ist. Wenn Sie versuchen von außerhalb auf einen Bezeichner aus einer Basisklasse zuzugreifen, haben Sie diesen Effekt nicht. Diesbezüglich müssen Sie sich um den späteren Nutzer also keine Gedanken machen.

Innerhalb von Methoden verwenden wir für den Zugriff auf Basisklassenbezeichner den this-Zeiger. Die explizite Angabe der Basisklasse ist beispielsweise beim Zugriff auf Typen aus der Basisklasse notwendig. Um die Übersicht zu erhöhen, macht man üblicherweise die typedefs der Basisklasse, in einer abgeleiteten Klasse, wiederum mittels typedef, unter dem gleichen Namen verfügbar. Von den Änderungen sind in unseren beiden Proxyklassen die nachfolgenden Methoden betroffen. Ihre neuen Implementierungen sehen so aus:

public: // In row_const_proxy
    row_const_proxy(matrix_type const& matrix, size_type const& row):
        detail::line_const_proxy< matrix_type >(matrix, row){}

    size_type const pos()const{ return detail::line_const_proxy< matrix_type >::pos(); }

    size_type const columns()const{ return this->matrix_const().columns(); }

public: // In column_const_proxy
    column_const_proxy(matrix_type const& matrix, size_type const& column):
        detail::line_const_proxy< matrix_type >(matrix, column){}

    size_type const pos()const{ return detail::line_const_proxy< matrix_type >::pos(); }

    size_type const rows()const{ return this->matrix_const().rows(); }

Proxys für nicht-konstante Matrizen

[Bearbeiten]
Letztlich verwendete Vererbungshierarchie

Da wir uns nun schon mal die Mühe gemacht haben, Codeverdopplung durch die gemeinsame Basisklasse line_const_proxy zu vermeiden, werden wir natürlich auch für diese beiden Proxys eine gemeinsame Basisklasse schaffen. Da einige der Funktionen wiederum mit denen der Proxys für konstante Matrizen identisch sind, wäre es natürlich angenehm, wenn wir diesen Code wiederum verwenden könnten. Eine Ableitung der Klassen von ihren jeweiligen Äquivalenten für konstante Matrizen, würde uns zwar ein paar Vorteile bieten, aber auch einen unangenehmen Nachteil. Genaugenommen ist dieser Nachteil nicht einmal für uns unangenehm, sondern für den Nutzer unseres Codes. Wenn der Compiler nämlich eine Fehlermeldung bezüglich einer nicht möglichen Typumwandlung, wie in der oben gezeigten Funktion f3(), generiert, dann würde diese bei nicht-öffentlicher Vererbung sehr wahrscheinlich einen Hinweis, auf die nicht zugreifbare Basisklasse enthalten. Der Nutzer erwartet nicht, dass diese Klassen verwandt sind und wenn er noch unerfahren ist, dürfte ihn eine solche Fehlermeldung ziemlich verwirren. Daher sollten wir dies vermeiden.

Wir werden stattdessen eine Klasse line_proxy erstellen und unsere beiden Proxys von dieser ableiten. Die Klasse line_proxy soll natürlich von line_const_proxy abgeleitet werden. Im Gegensatz zur Vererbung der beiden Proxys für konstante Matrizen, muss die Vererbung diesmal geschützt (protected) erfolgen, denn wir wollen von line_proxy ja weitere Klassen ableiten.

line_proxy muss zwei Member besitzen. Zum Einen den üblichen Konstruktor, der eine Matrix und eine Position erhält und zum Anderen eine Zugriffsmethode, die uns eine nicht-konstante Referenz auf das referenzierte Matrixobjekt bietet. Wir werden in dieser Methode die Konstantheit der Matrix mittels const_cast entfernen. Dies ist allerdings nur sicher, wenn wir sicher wissen, dass die Matrix in Wahrheit gar nicht konstant ist. Um dies sicherzustellen, bedienen wir uns des ersten Parameters unseres Konstruktors. Im Gegensatz zum Konstruktor der Basisklasse, wird der Konstruktor von line_proxy eine nicht-konstante Referenz auf ein matrix-Objekt übernehmen. Somit ist es unmöglich, das Objekte von line_proxy eine Matrix referenzieren können, die konstant ist. Gleiches muss natürlich auch für alle Klassen gelten, die von line_proxy abgeleitet werden.

template < typename Matrix >
class line_proxy: protected line_const_proxy< Matrix >{
protected:
    typedef Matrix                      matrix_type;
    typedef typename Matrix::value_type value_type;
    typedef typename Matrix::size_type  size_type;

    line_proxy(matrix_type& matrix, size_type const& pos):
        detail::line_const_proxy< matrix_type >(matrix, pos){}

    matrix_type& matrix()const{ return const_cast< matrix_type& >(this->matrix_const()); }
};

In den abgeleiteten Klassen können wir nun ganz bequem die Methoden matrix() oder matrix_const() verwenden, je nachdem, ob wir Schreibzugriff benötigen oder nicht. Die Ableitung selbst erfolgt jetzt wieder privat. Die meisten Member entsprechen im Wesentlichen denen, der konstanten Proxyklasse und bedürfen daher keiner näheren Betrachtung.

Worüber wir uns noch einmal kurz Gedanken machen müssen, ist die Möglichkeit ein Proxyobjekt, für nicht-konstante Matrizen, implizit in ein entsprechendes Proxyobjekt, für konstante Matrizen, umzuwandeln. Wir können dies realisieren, indem wir den Proxys für nicht konstante Matrizen einen weiteren Konstruktor verpassen und diesen als friend der entsprechenden hiesigen Proxyklassen deklarieren, so dass dieser auf die protected-Methode matrix_const() zugreifen kann. Sehr viel einfacher und übersichtlicher ist es jedoch, wenn wir einfach einen entsprechenden Typcastoperator für die Proxys für konstante Matrizen definieren. Unsere Implementieren sieht somit folgendermaßen aus.

template < typename Matrix >
class row_proxy: private detail::line_proxy< Matrix >{
public:
    typedef Matrix                      matrix_type;
    typedef typename Matrix::value_type value_type;
    typedef typename Matrix::size_type  size_type;

    row_proxy(matrix_type& matrix, size_type const& row):
        detail::line_proxy< matrix_type >(matrix, row){}

    value_type& operator[](size_type const& column_number)const{ return column(column_number); }
    value_type& column(size_type const& number)const{ return *(begin() + number); }

    size_type const columns()const{ return this->matrix_const().columns(); }
    size_type const size()const{ return columns(); }

    size_type const pos()const{ return detail::line_proxy< matrix_type >::pos(); }

    operator row_const_proxy< matrix_type >(){
        return row_const_proxy< matrix_type >(this->matrix_const(), this->pos());
    }
};

template < typename Matrix >
class column_proxy: private detail::line_proxy< Matrix >{
public:
    typedef Matrix                      matrix_type;
    typedef typename Matrix::value_type value_type;
    typedef typename Matrix::size_type  size_type;

    column_proxy(matrix_type& matrix, size_type const& column):
        detail::line_proxy< matrix_type >(matrix, column){}

    value_type& operator[](size_type const& row_number)const{ return row(row_number); }
    value_type& row(size_type const& number)const{ return *(begin() + number); }

    size_type const rows()const{ return this->matrix_const().rows(); }
    size_type const size()const{ return rows(); }

    size_type const pos()const{ return detail::line_proxy< matrix_type >::pos(); }

    operator column_const_proxy< matrix_type >(){
        return column_const_proxy< matrix_type >(this->matrix_const(), this->pos());
    }
};

Elementweise tauschen

[Bearbeiten]

Wie bereits erwähnt wurde, werden wir die swap()-Funktion für unsere Proxys nicht selbst überladen. Allerdings wäre es durchaus Hilfreich, wenn wir eine Funktion zur Verfügung stellen würden, welche alle Elemente zweier Zeilen oder Spalten vertauscht. Wir werden diese Funktion element_swap() nennen. Das Vertauschen von Elemente ist natürlich nur für die beiden Proxys sinnvoll, die auf eine nicht-konstante Matrix verweisen. Daher benötigen wir zwei Überladungen, die ihrerseits natürlich Templates sein müssen, um mit allen Klasseninstanzen der jeweiligen Klassentemplats umgehen zu können. Der Quellcode für beide Funktionen ist jedoch identisch, daher wäre es schlechter Stiel, diesen zweimal einzugeben. Wie also lösen wir dieses Dilemma.

Wir können natürlich als Templateparameter einen Typ Proxy übergeben lassen. Dies hat allerdings den gewaltigen Nachteil, dass unsere Funktion dann kompatibel zu allen Typen ist, welche die in der Implementierung verwendeten Member unterstützen. Außerdem werden die Fehlermeldungen für inkompatible Typen sehr unübersichtlich und für unerfahrene Programmierer schwer zu interpretieren. Was wir wollen ist, diese allgemeine Funktion vor dem Nutzer zu verstecken. Die beiden auf die Proxys bezogenen Überladungen können dann diese allgemeine Funktion aufrufen. Wir packen die allgemeine Funktion daher in den Namensraum detail.

namespace detail{
    template < typename Proxy >
    inline void element_swap_template(Proxy const& lhs, Proxy const& rhs){
        typedef typename Proxy::iterator iterator;
        using std::swap;

        if(lhs.size() != rhs.size()){
            throw error::size_unequal("mitrax::element_swap_template<>()", lhs.size(), rhs.size());
        }

        for(
            iterator i = lhs.begin(), j = rhs.begin();
            i != lhs.end();
            ++i, ++j
        ){
            swap(*i, *j);
        }
    }
}

template < typename Matrix >
inline void element_swap(row_proxy< Matrix > const& lhs, row_proxy< Matrix > const& rhs){
    detail::element_swap_template(lhs, rhs);
}

template < typename Matrix >
inline void element_swap(column_proxy< Matrix > const& lhs, column_proxy< Matrix > const& rhs){
    detail::element_swap_template(lhs, rhs);
}

Zum eigentlichen Vertauschen verwenden wir selbstverständlich wieder das Iteratorinterface. Die Ausnahmeklasse error::size_unequal wird später behandelt. Sie ist von std::logic_error abgeleitet. Wie Ihnen vielleicht auffällt, müssen wir uns um den Namen oder die einfache Nutzung von element_swap_template viel weniger Gedanken machen als sonst. Denn außer uns und jenen Menschen, die unseren Code später möglicherweise warten müssen, wird nie jemand von dieser Funktion erfahren. Natürlich heißt dies nicht, dass Sie sich überhaupt keine Gedanken machen müssen. Eine kleine Anzahl von Leuten wird die Funktion schließlich immer noch zu Gesicht bekommen und diese sollten schon verstehen können, was vor sich geht.