C++-Programmierung/ Eine Matrix-Bibliothek – mitrax/ Ein- und Ausgabe
Wir werden uns in diesem Kapitel mit der Ein- und Ausgabe von Matrizen und deren Proxyklassen beschäftigen. Die Ein-/Ausgabe soll dabei in einem Klartextformat erfolgen. Die Boost-Bibliothek uBLAS repräsentiert Strukturen aus der linearen Algebra, darunter auch Matrizen. Wir werden uns an deren Ein-/Ausgabeformat orientieren, so dass ein Austausch mit Programmen, die diese Bibliothek nutzen, problemlos möglich ist. Als Vorbedingung sei noch angemerkt, dass das Ein- und das Ausgabeformat für einen Datentyp prinzipiell identisch oder zumindest kompatibel sein sollte. Auf diese Weise können Daten ausgegeben und später wieder eingelesen werden, ohne Sie manipulieren zu müssen.
Das Matrizenformat
[Bearbeiten]Jede Matrix in Textform beginnt mit einer öffnenden eckigen Klammer ([
). Dann folgt die Anzahl der Zeilen als vorzeichenlose Ganzzahl. Anschließend muss ein Komma folgende (,
). Danach die Anzahl der Spalten als vorzeichenlose Ganzzahl. Gefolgt von einer schließenden eckigen Klammer (]
). Diesen Teil bezeichnen wir als Header, da er die Dimension der Matrix enthält.
Auf den Header folgt der Körper der Matrix. Er besteht aus einer beliebigen Anzahl gleichgroßer Zeilen, wobei gleichgroß sich auf die Anzahl der enthaltenen Elemente bezieht. Die Zeilen werden komplett in runde Klammern eingefasst und sind durch Kommata separiert. Jede Zeile ist wiederum in runde Klammern eingefasst und die Elemente sind wiederum durch Kommata separiert. Das Format der Elemente hängt vom jeweiligen Datentyp der Matrixelemente ab und kann daher nicht allgemein beschrieben werden. Die genannten Zeichen und die Elemente, sowie die vorzeichenlosen Ganzzahlen, werden als Token bezeichnet. Alle Token des Formats können durch eine beliebige Anzahl von Whitespaces getrennt sein.
Im Folgenden sehen Sie Beispiele für eine, aus ganzzahligen Werten bestehende, Matrix, sowie eine für eine Matrix, die Elemente von Typ std::complex< int >
enthält. Die beiden Matrizen werden jeweils in einer kompakten und einer übersichtlichen Schreibweise dargestellt. Beide Schreibweisen entsprechen dem vorgegeben Format.
// Matrix aus Ganzzahlen [3,3]((2,4,6),(8,10,12),(14,16,18)) [3,3]( ( 2, 4, 6), ( 8, 10, 12), ( 14, 16, 18) ) // Matrix aus komplexen Werten [3,3](((3,0),(6,9),(9,36)),((12,81),(15,144),(18,225)),((21,324),(24,441),(27,576))) [3,3]( ( (3,0), (6,9), (9,36)), ( (12,81), (15,144), (18,225)), ( (21,324), (24,441), (27,576)) )
Beide Varianten sollen durch mit dem Eingabeoperator funktionieren und der Ausgabeoperator soll, abhängig von zwei gesetzten Ausgabemanipulatoren, in der Lage sein, beide Ausgaben zu produzieren. Die Erste eignet sich gut für einen Austausch zwischen verschiedenen Programmen oder zur Speicherung für die spätere Verwendung. Die zweite Ausgabevariante ist gut für eine Betrachtung durch einen Menschen geeignet und lässt sich praktischerweise auch hervorragend in eine Tabellenkalkulation übertragen.
Ausgabemanipulatoren
[Bearbeiten]Ehe wir uns der Ausgabe selbst widmen, werden wir uns noch der Möglichkeit zu Manipulation derselben zuwenden. Diese stehen in der Datei iomanip.hpp
. Es wurde ja bereits gesagt, dass wir zwei Ausgabemanipulatoren für unsere Matrix schreiben wollen. Der Erste bestimmt die Mindestbreite, die ein Matrixelement belegen soll. Auf diese Weise kommt oben die gleichmäßige Spaltenbreite zu Stande. Der zweite Manipulator soll einen mehrzeiligen Ausgabemodus aktivieren bzw. deaktivieren, so dass, falls gewünscht, jede Matrixzeile auch in der Ausgabe auf einer eigenen Zeile steht.
Für beide Manipulatoren, müssen wir Informationen in dem Ausgabestream speichern, auf dem wir unsere Matrix ausgeben wollen. Die C++-Standardbibliothek bietet zu diesem Zweck in der Klasse std::ios_base
eine statische Methode xalloc()
an. Diese gibt uns einen eindeutigen Index zurück, der auf ein long
verweist, in dem wir Daten für unsere Zwecke unterbringen können. Der Ausgabestream selbst, erlaubt uns mittels der Methode iword()
auf diesen long
zuzugreifen. Die Methode bekommt den eindeutigen Index als Parameter und gibt eine Referenz auf den long
zurück.
Nehmen wir uns zunächst den Manipulator für die Elementmindestbreite von. Um den Zugriff auf unsere Speicherposition einfach zu gestalten, verpacken wir die eben genannten Methoden in einer Funktion, die uns eine Referenz auf den long
zurückgibt. Wir platzieren diese Funktion im Namensraum detail
und nennen sie element_width_flag()
. Die Funktion enthält eine statische konstante Variable für den eindeutigen Index. Diese wird beim ersten Aufruf der Methode mit einem gültigen Index initialisiert. Außerhalb der Methode kann somit kein direkter Zugriff auf die Speicherposition erfolgen, da diese nicht bekannt ist. Innerhalb der Funktion müssen wir nun nur noch die Methode iword()
unseren Ausgabestream mit dem eindeutigen Index aufrufen und schon können wir die gewünschte Referenz zurückliefern.
long& element_width_flag(std::ios_base& s){
static int const unique_index = std::ios_base::xalloc();
return s.iword(unique_index);
}
Als nächstes legen wir eine Funktion zum Lesen und eine zum Schreiben auf die entsprechende Speicherstelle eines Streams an. Diese beiden Methoden können vom Nutzer verwendet werden und stehen daher im direkt Namensraum mitrax
.
long get_element_width_flag(std::ios_base& s){
return detail::element_width_flag(s);
}
void set_element_width_flag(std::ios_base& s, long n){
detail::element_width_flag(s) = n;
}
Das würde genügen, um eine entsprechende Ausgabe realisieren zu können, jedoch wollen wir den Wert gern direkt als Manipulator auf den Ausgabestream schieben können. So wie es etwa bei std::setw()
der Fall ist. Wir müssen daher eine Helferklasse schreiben, die ein Objekt erzeugt, welches dann als Parameter an einen Ausgabeoperator übergeben werden kann. Dieser wird schließlich die Set-Funktion aufrufen. Der Ausgabeoperator muss dabei als Template realisiert sein, um alle Arten von Streams zu unterstützen. Das ganze Konstrukt sieht daher so aus:
class set_element_width{
public:
set_element_width(long width):width_(width){}
private:
long const width_;
template < class charT, class traits >
friend std::basic_ostream< charT, traits >& operator<<(
std::basic_ostream< charT, traits >& os, set_element_width const& f
){
set_element_width_flag(os, f.width_);
return os;
}
};
Die Definition des befreundeten Funktionstemplates innerhalb der Klasse ist reine Bequemlichkeit. Insbesondere spielt es keine Rolle, dass die Funktion im private
-Bereich definiert ist. Die Einrückung wurde hier lediglich vorgenommen, um Verwirrung zu vermeiden. Es sein außerdem noch einmal daran erinnert, dass bei der Definition einer Funktion innerhalb einer Klassendefinition, die Funktion implizit inline
ist, daher müssen wir dies hier nicht nochmal explizit angeben. Oft wird vergessen, dass diese Regel auch auf Nicht-Member wie friend
und static
-Funktionen zutrifft.
Die Definition des anderen Manipulators ist weitgehend analog. Da wir hier lediglich ein Bit Information benötigen, werden wir es so einrichten, dass später eventuelle weitere Flags dieser Art, an der eindeutigen Speicherstelle abgelegt werden können. Wir Verwenden eine Enumeration um die Bitposition innerhalb der Speicherstelle festzuhalten. Auch diese Information geht den Benutzer nichts an, daher steht auch die Enumeration im Namensraum detail
. Das Ganze sieht wie Folgt aus:
namespace detail{
struct format_flags{
static long const multi_line = 0x01;
};
long& format_flags_flag(std::ios_base& s){
static int const unique_index = std::ios_base::xalloc();
return s.iword(unique_index);
}
}
bool get_multi_line_flag(std::ios_base& s){
return detail::format_flags_flag(s) & detail::format_flags::multi_line;
}
void set_multi_line_flag(std::ios_base& s, bool f){
if(f){
detail::format_flags_flag(s)
Da wir diesmal nur zwei mögliche Zustände haben, wäre es angenehm, wenn wir nicht jedes mal ein Objekt erzeugen müssten, wenn wir den Ausgabemodus ändern wollen. Daher stellen wir die zwei möglichen Instantiierungen der Klasse als Variablen bereit. Diese lassen sich dann Verwenden wie std::endl
.
Um eine der formatierten Ausgaben von oben, aus einer Matrix m
auf dem Ausgabestream std::cout
zu erzeugen, können wir folgendes schreiben, sobald wir die Ausgabe selbst realisiert haben.
Der restliche Text des Kapitels bezieht sich auf Inhalte der Datei io.hpp
.
Ausgabe von Matrizen
[Bearbeiten]Wir werden die Ausgabe zunächst auf einem Puffer realisieren und diesen zum Schluss, auf dem eigentlichen Stream ausgeben. Das macht die Ausgabe gelegentlich etwas effizienter und falls irgendwo eine Ausnahme geworfen wird, vermeiden wir, dass nur ein Teil der Matrix ausgegeben wird. Das eine Ausnahme geworfen wird, kann beispielsweise im Ausgabeoperator eines Elements passieren, über diesen wissen wir ja nichts. Wenn wir einen Puffer verwenden, wird unsere Matrix entweder vollständig oder gar nicht ausgegeben.
Als Puffer verwenden wir einen passenden Stringstream. Dieser muss vom eigentlichen Ausgabestream die aktuellen Flags, die Lokalisierungseinstellungen und die Gleitkommagenauigkeit übernehmen. Außerdem müssen wir uns natürlich unsere Manipulatoren aus dem Originalstream extrahieren. Diese speichern wir allerdings gleich in zwei Variablen, da wir diese Informationen ja direkt verwenden wollen. Bei der Ausgabe werden die Matrixelemente alle einmal durchlaufen, daher können wir einen Matrixiterator verwenden. Auf diese Weise müssen wir nicht jedes mal aufwendig die aktuelle Position innerhalb der Matrix berechnen. Die Ausgabe erfolgt ja genau in der Reihenfolge, in der der Iterator durch die Matrix läuft. Um die Übersicht ein wenig zu steigern, werden wir außerdem vorweg die Anzahl der Zeilen und Spalten in zwei Konstanten speichern.
Da der Zugriff auf die Typen innerhalb der Matrix relativ kompliziert aussieht, werden wir die benötigten Typen per typedef
zur Verfügung stellen. Auch das sollte einer besseren Übersicht zuträglich sein. Als Erstes geben wir den Header der Matrix aus und anschließend, unter Berücksichtigung der beiden Manipulatoren, den Körper der Matrix. Zum Schluss müssen wir nur noch unseren Puffer auf dem eigentlichen Ausgabestream ausgeben und natürlich wie üblich diesen wieder als Referenz zurückgeben, damit weitere Ausgaben verkettet werden können.
template < class charT, class traits, typename T, typename Container >
inline std::basic_ostream< charT, traits >&
operator<<(std::basic_ostream< charT, traits >& os, matrix< T, Container > const& m){
typedef typename matrix< T, Container >::size_type size_type;
typedef typename matrix< T, Container >::const_iterator const_iterator;
// Ausgabe erfolgt zunächst auf Puffer
std::basic_ostringstream< charT, traits, std::allocator< charT > > tos;
tos.flags(os.flags());
tos.imbue(os.getloc());
tos.precision(os.precision());
// Manipulatoren
long const output_width = get_element_width_flag(os);
bool const multi_line = get_multi_line_flag(os);
// Schneller Zugriff
size_type const rows = m.rows();
size_type const columns = m.columns();
// Elementweise durchgehen mittels Iterator
const_iterator iter = m.begin();
// Ausgabe Header
tos << '[' << rows << ',' << columns << "](";
if(multi_line) tos << '\n';
// Ausgabe erste Zeile
if(rows){
tos << '(';
// Ausgabe erstes Element
if(columns){
tos << std::setw(output_width) << *iter++;
}
// Ausgabe restliche Element
for(typename matrix< T, Container >::size_type j = 1; j < columns; ++j){
tos << ',' << std::setw(output_width) << *iter++;
}
tos << ')';
}
// Ausgabe restliche Zeilen
for(typename matrix< T, Container >::size_type i = 1; i < rows; ++i){
tos << ',';
if(multi_line) tos << '\n';
tos << '(';
// Ausgabe erstes Element
if(columns){
tos << std::setw(output_width) << *iter++;
}
// Ausgabe restliche Element
for(typename matrix< T, Container >::size_type j = 1; j < columns; ++j){
tos << ',' << std::setw(output_width) << *iter++;
}
tos << ')';
}
if(multi_line) tos << '\n';
tos << ')';
// Ausgabe auf Stream und Streamrückgabe
return os << tos.str().c_str();
}
Über den Datentyp der Zeichen müssen Sie sich übrigens keine Gedanken machen, diese werden bei der Ausgabe implizit in den richtigen Datentyp konvertiert.
Eingabe von Matrizen
[Bearbeiten]Die Eingabe von Matrizen verlangt des Öfteren, dass als nächstes ein bestimmtes Zeichen folgen muss. Diese Prüfung können wir in eine Hilfsfunktion auslagern, welche natürlich wieder im Namensraum detail
unterzubringen ist. Wir werden Sie innerhalb des Eingabeoperators mittels using
-Deklaration verfügbar machen. Die Funktion bekommt den Eingabestream und das erwartete Zeichen als Parameter. Wenn der Stream nicht in Ordnung ist, gibt die Funktion sofort false
zurück. Andernfalls wir ein Zeichen eingelesen und, sofern die Eingabe geklappt hat, überprüft ob dieses das erwartete Zeichen ist. Ist dies der Fall, wird true
zurückgegeben. Andernfalls wird das Zeichen in den Stream zurückgeschriebenen und das fail
-Bit gesetzt. Die Funktion gibt dann false
zurück.
template < class charT, class traits >
inline bool input_equal(std::basic_istream< charT, traits >& is, charT const& should_be){
if(!is) return false;
charT in;
if(is >> in && in == should_be) return true;
is.putback(in);
is.setstate(std::ios_base::failbit);
return false;
}
Die Eingabefunktion selbst wird dadurch einigermaßen Überschaubar. Wir nutzen zunächst wieder typedef
um die benötigten Typen abzukürzen und legen uns zwei Variablen für die Anzahl der Zeilen und Spalten an. Die Dimension der einzugebenden Matrix ist unabhängig von der vorherigen Dimension des Matrixobjekts. In einer Kette von Eingaben, innerhalb einer if
-Bedingung, lesen wir nun den Header ein. Schlägt eine der Operationen fehl, ist die Bedienung erfüllt und der Stream wird zurückgegeben. Das fail
-Bit ist bei der fehlgeschlagenen Eingabe an dieser Stelle bereits gesetzt wurden, daher müssen wir dies nicht mehr tun.
Wenn die Headereingabe geklappt hat, dann müssen als nächstes die Elemente eingelesen werden. Auch hier kann es wieder passieren, dass irgendwo eine Ausnahme geworfen wird oder die Eingabe schlicht ein fehlerhaftes Format besitzt und die Eingabe deshalb abgebrochen wird. In jedem Fall sollten wir sichergehen, dass wir alle Elemente korrekt einlesen konnten, bevor wir das matrix
-Objekt tatsächlich verändern. Wir werden die Werte daher zunächst in einen Container einlesen. Auch hier können wir wieder einen Iterator verwenden und uns darauf verlassen, dass dieser immer auf das richtige Element zeigt. Wenn die Eingabe erfolgreich war, reinitiallisieren wir unser matrix
-Objekt mit der neuen Dimension und den neuen Daten. Dabei werden einfach die Container getauscht, so dass wir hier höchstwahrscheinlich keine aufwendige Kopie aller Elemente durchführen müssen. Das späte setzen der Daten ist somit nahezu kostenlos. Zum Schuss müssen wir natürlich auch wieder den Eingabestream zurückgeben.
template < class charT, class traits, typename T, typename Container >
inline std::basic_istream< charT, traits >&
operator>>(std::basic_istream< charT, traits >& is, matrix< T, Container >& m){
using detail::input_equal;
typedef typename matrix< T, Container >::size_type size_type;
typedef typename matrix< T, Container >::const_iterator const_iterator;
typedef std::vector< T > container_type;
// Neue Dimension der Matrix
size_type rows;
size_type columns;
// Header einlesen
if(
!input_equal(is, charT('[')) ||
!(is >> rows) ||
!input_equal(is, charT(',')) ||
!(is >> columns) ||
!input_equal(is, charT(']')) ||
!input_equal(is, charT('('))
) return is;
// Daten erst komplett Einlesen und dann gegen Originale tauschen
container_type elements(rows * columns);
typename container_type::iterator tmp_iter = elements.begin();
// Lese n-1 Zeilen
for(size_type i = size_type(); i < rows-1; ++i){
if(
!input_equal(is, charT('('))
) return is;
// Lese n-1 Spalten
for(size_type j = size_type(); j < columns-1; ++j){
if(is >> *tmp_iter++ && input_equal(is, charT(','))) continue;
return is;
}
if( // Lese n-te Spalten
!(is >> *tmp_iter++) ||
!input_equal(is, charT(')')) ||
!input_equal(is, charT(','))
) return is;
}
// Lese n-te Zeilen
if(
!input_equal(is, charT('('))
) return is;
// Lese n-1 Spalten
for(size_type j = size_type(); j < columns-1; ++j){
if(is >> *tmp_iter++ && input_equal(is, charT(','))) continue;
return is;
}
if( // Lese n-te Spalten
!(is >> *tmp_iter++) ||
!input_equal(is, charT(')')) ||
!input_equal(is, charT(')'))
) return is;
// Eingelesene Werte gegen Matrixwerte tauschen
m.reinit(rows, columns, elements);
return is;
}
Beachten Sie, dass Sie sich bei der Eingabe sehr wohl Gedanken über den Datentyp der Literale machen müssen. Da wir keine brauchbare Möglichkeit haben, für unterschiedliche Datentypen auch unterschiedliche Literale zu verwenden, greifen wir auf eine Typumwandlung zurück. Diese realisieren wir mittels temporärer Objekterzeugung. Speziell für nicht eingebaute Datentypen ist dies die richtige Aussage und nebenbei sind diese auch der Grund, warum es nach dem aktuellen C++-Standard unmöglich wäre, unterschiedliche Literale zu nutzen. Benutzerdefinierte Literale werden erst mit dem kommenden C++-Standard eingeführt und derzeit gibt es keinen Compiler, der dies bereits beherrscht.
Sie haben nun die Möglichkeit Matrizen formatiert auszugeben und Sie später wieder einzulesen. Wenn Sie möchten können Sie ja mal das oben gezeigt Beispiel, für eine Ausgabe mit aktiven Manipulatoren, ausprobieren. Das sollte jetzt funktionieren.
Proxys ausgeben
[Bearbeiten]Das Ein-/Ausgabeformat für die Proxyklassen entspricht dem uBLAS-Format für Vektoren. In eckigen Klammern wird die Anzahl der Elemente angegeben und anschließen in runden Klammern eingeschlossen und durch Kommata separiert, die Elemente. Auch hier können zwischen den Token natürlich wieder beliebig viele Whitespaces stehen. Es lässt sich also nicht mehr erkennen, ob es sich um einen Zeilen- oder einen Spaltenproxy gehandelt hat. Wir können die beiden Ausgaben also in einem gemeinsamen Funktionstemplate realisieren.
Leider müssen wir für jede unserer vier Proxyklassen eine eigene Definition für den Ausgabeoperator angeben, da ja für Funktionstemplates nicht auf eventuell mögliche implizite Typumwandlungen geprüft wird. Wir müssen daher für die Proxys für nicht-konstante Matrizen selbst eine Definition schreiben, welche die Typumwandlung durchführt und dann die entsprechende Version mit Proxys für konstante Matrizen aufruft. Diese werden wiederum eine Funktionsinstanz des Funktionstemplats aufrufen, dass die Ausgabe richtungsunabhängig realisiert. Diese Template steht natürlich im Namensraum detail
.
Das Funktionstemplate bekommt einen Iterator auf den Anfang der Zeile bzw. Spalte und natürlich deren Länge. Dies sind entsprechend auch die zusätzlichen Templateparameter. Die Ausgabe erfolgt zunächst wieder auf einen Puffer. Wir beachten die Ausgabemanipulatoren für minimal Elementbreite und den Mehrzeilenmodus. Ist letzter aktiv, geben wir den Header und den Körper jeweils in einer eigenen Zeile aus. Das ganze sieht folgendermaßen aus.
namespace detail{
template < class charT, class traits, typename Iterator, typename Size >
inline std::basic_ostream< charT, traits >&
proxy_output(std::basic_ostream< charT, traits >& os, Iterator iter, Size const& size){
// Ausgabe erfolgt zunächst auf Puffer
std::basic_ostringstream< charT, traits, std::allocator< charT > > tos;
tos.flags(os.flags());
tos.imbue(os.getloc());
tos.precision(os.precision());
// Manipulatoren
long const output_width = get_element_width_flag(os);
bool const multi_line = get_multi_line_flag(os);
// Ausgabe Header
tos << '[' << size << ']';
if(multi_line) tos << '\n';
// Ausgabe der Zeile / Spalte
tos << '(';
if(size){
tos << std::setw(output_width) << *iter;
++iter;
}
for(Size i = Size()+1; i < size; ++i){
tos << ',' << std::setw(output_width) << *iter;
++iter;
}
tos << ')';
// Ausgabe auf Stream und Streamrückgabe
return os << tos.str().c_str();
}
}
template < class charT, class traits, typename Matrix >
inline std::basic_ostream< charT, traits >&
operator<<(std::basic_ostream< charT, traits >& os, row_const_proxy< Matrix > const& proxy){
return detail::proxy_output(os, proxy.cbegin(), proxy.columns());
}
template < class charT, class traits, typename Matrix >
inline std::basic_ostream< charT, traits >&
operator<<(std::basic_ostream< charT, traits >& os, column_const_proxy< Matrix > const& proxy){
return detail::proxy_output(os, proxy.cbegin(), proxy.rows());
}
template < class charT, class traits, typename Matrix >
inline std::basic_ostream< charT, traits >&
operator<<(std::basic_ostream< charT, traits >& os, row_proxy< Matrix > const& proxy){
return os << row_const_proxy< Matrix >(proxy);
}
template < class charT, class traits, typename Matrix >
inline std::basic_ostream< charT, traits >&
operator<<(std::basic_ostream< charT, traits >& os, column_proxy< Matrix > const& proxy){
return os << column_const_proxy< Matrix >(proxy);
}
Die Ausgabe bietet keine großen Überraschungen. Im wesentlichen kombinieren wir ein paar der Techniken, die wir zuvor kennengelernt haben. Neues kommt an dieser Stelle nicht hinzu.
Proxys einlesen
[Bearbeiten]Beim Einlesen einer Zeile oder Spalte müssen wir beachten, dass wir die Länge nicht ändern können. Der eingegebene Vektor muss längenmäßig zu dem Proxy passen, in den eingelesen werden soll. Ist dies nicht gegeben werden wir die Eingabe abbrechen und das fail
-Bit für den Stream setzen. Der Vergleich zwischen einer einzugebenden und einer vorgegeben Zahl, erinnert an das Einlesen erwarteter Zeichen und in der Tat, werden wir auch diesmal eine Helferfunktion gleichen Namens verwenden. Es ist zwar die einzige Stelle, an der innerhalb unserer Ein-/Ausgabefunktionen etwas anderes als ein Zeichen mit einem bestimmten Wert erwartet wird, aber es erhöht dennoch die Lesbarkeit des Quellcodes, eine Funktion mit verständlichem Namen für diese Aktion zu verwenden.
Wir lesen unser Elemente wieder zuerst vollständig in ein temporäres Feld. Diesmal verwenden wir dafür einfach einen std::vector
. Da die Elemente am Ende ohnehin zugewiesen werden müssen, würde sich kein Vorteil daraus ergeben den container_type
von matrix
zu verwenden und gleichzeitig könnten wir eventuelle Nachteile nicht Abschätzen. Ansonsten gibt es auch hier keine großen Überraschungen. Die beiden Operatorüberladungen rufen wieder eine richtungsunabhängig geschrieben Funktionsinstanz eines Funktionstemplats aus detail
aus und den Quellcode kennen Sie vom Prinzip her inzwischen auch schon.
namespace detail{
template < class charT, class traits, class ShouldBe >
inline bool input_equal(std::basic_istream< charT, traits >& is, ShouldBe const& should_be){
ShouldBe in;
if(is >> in && in == should_be) return true;
is.setstate(std::ios_base::failbit);
return false;
}
template < class charT, class traits, typename Iterator, typename Size >
inline std::basic_istream< charT, traits >&
proxy_input(std::basic_istream< charT, traits >& is, Iterator iter, Size const& size){
typedef std::vector< typename Iterator::value_type > container_type;
// Header einlesen und Länge prüfen
if(
!input_equal(is, charT('[')) ||
!input_equal(is, size) ||
!input_equal(is, charT(']')) ||
!input_equal(is, charT('('))
) return is;
// Werte zunächst komplett in temporären Container einlesen
container_type elements(size);
typename container_type::iterator tmp_iter = elements.begin();
for(Size i = Size(); i < size-1; ++i){
if(is >> *tmp_iter++ && input_equal(is, charT(','))) continue;
return is;
}
if(
!(is >> *tmp_iter++) ||
!input_equal(is, charT(')'))
) return is;
// Eingelesene Werte übertragen
tmp_iter = elements.begin();
for(Size i = Size(); i < size; ++i){
*iter = *tmp_iter;
++tmp_iter;
++iter;
}
return is;
}
}
template < class charT, class traits, typename Matrix >
inline std::basic_istream< charT, traits >&
operator>>(std::basic_istream< charT, traits >& is, row_proxy< Matrix > proxy){
return detail::proxy_input(is, proxy.begin(), proxy.columns());
}
template < class charT, class traits, typename Matrix >
inline std::basic_istream< charT, traits >&
operator>>(std::basic_istream< charT, traits >& is, column_proxy< Matrix > proxy){
return detail::proxy_input(is, proxy.begin(), proxy.rows());
}
Somit verfügen Sie nun über ein komplettes Ein-/Ausgabeinterface und können auch endlich etras von Ihren Matrizen und Proxyklassen sehen. Als nächstes werden wir ein recht seperates Thema, die Ausnahmebahandlung besprechen und anschließend führen wie einige grundlegende Rechenoperationen auf unseren Matrizen ein. Sie sollten danach in der Lage sein, mitrax selbst um beliebige Operationen zu erweitern.