Probleme mit eigener Matrix-Klasse
-
Hallo,
Vor (sehr) längerem hatte ich mit Eurer Hilfe eine rudimentäre Matrix-Klasse entwickelt, die wollte ich heute mal benutzen, bekomme aber Probleme in const-Methoden.Die Klasse Matrix sieht so aus:
#pragma once #include <iostream> #include <iomanip> #include <string> #include <vector> #include <algorithm> #include <exception> template <typename T> class Matrix_T { std::size_t rows_ = 0; std::size_t columns_ = 0; std::vector<T> elements_; public: class Row_T { T* row_data; std::size_t cols_ = 0; public: Row_T(T* row_data, const std::size_t columns) : row_data{ row_data }, cols_{ columns } {} T& operator[](const std::size_t column) { if (column >= cols_) throw std::out_of_range("operator[](): column: " + std::to_string(column)); return row_data[column]; } }; Matrix_T() = default; Matrix_T(const std::size_t rows, const std::size_t columns, const T& v = {}) : rows_{ rows }, columns_{ columns }, elements_(rows * columns, v) {} Matrix_T(const std::initializer_list<std::initializer_list<T>>& elements) : Matrix_T{ elements.size(), std::max(elements, [](const auto& row, const auto& column) { return row.size() < column.size(); }).size() } { std::size_t r = 0; for (const auto& row : elements) { if (r >= rows_) break; std::size_t c = 0; for (const auto& v : row) { if (c >= columns_) break; (*this)[r][c] = v; ++c; } ++r; } } std::size_t rows() const { return rows_; } std::size_t columns() const { return columns_; } T operator()(const std::size_t row, const std::size_t column) const { const std::size_t idx = toLinearIndex(row, column); return elements_[idx]; } T& operator()(const std::size_t row, const std::size_t column) { const std::size_t idx = toLinearIndex(row, column); return elements_[idx]; } Row_T operator[](const std::size_t row) { if (row >= rows_) throw std::out_of_range("operator[](): row: " + std::to_string(row)); return { &elements_[row * columns_], columns_ }; } Matrix_T operator*(const Matrix_T& rhs) const { if (columns_ != rhs.rows_) throw std::exception("operator*()"); Matrix_T mat(rows_, rhs.columns_); T v = {}; for (std::size_t c = 0; c < rhs.columns_; ++c) { for (std::size_t r = 0; r < rows_; ++r) { v = (*this)(r, 0) * rhs(0, c); for (std::size_t l = 1; l < columns_; ++l) { v += (*this)(r, l) * rhs.element(l, c); } //mat[r][c] = v; mat(r, c) = v; } v = {}; } return mat; } private: std::size_t toLinearIndex(const std::size_t row, const std::size_t column) const { if (row >= rows_) throw std::out_of_range("toLinearIndex(): row: " + std::to_string(row)); if (column >= columns_) throw std::out_of_range("toLinearIndex(): column: " + std::to_string(column)); return columns_ * row + column; } }; template <typename T> std::ostream& operator<<(std::ostream& oStream, const Matrix_T<T>& mat) { oStream << "\n"; oStream << typeid(T).name() << " " << mat.rows() << "x" << mat.columns(); oStream << "\n{ "; for (std::size_t r = 0; r < mat.rows(); ++r) { if (r > 0) oStream << "\n{ "; for (std::size_t c = 0; c < mat.columns(); ++c) { if (c > 0) oStream << ", "; oStream << std::hex << std::uppercase << std::setw(4) << std::setfill('0') << mat(r, c); } oStream << " }"; } return oStream; }
Wenn ich diese Klasse in einer andereren verwende, kann ich in const-Methoden nicht den
[]operator
verwenden. Als Beispiel dieser kurze Code:#include "Matrix.h" #include <iostream> class Mat { Matrix_T<int> matrix; public: Mat() : matrix{ { 0, 1, 2 }, { 3, 4, 5 }, { 6, 7, 8 }} {} // geht nur ohne const // ansonsten Error C2678: binary '[': no operator found which takes a left - hand operand of type 'const Matrix_T<int>' // (or there is no acceptable conversion) void printVal(const std::size_t r, const std::size_t c) { std::cout << matrix[r][c] << '\n'; // error } }; int main() { Mat mat; mat.printVal(2, 0); }
Ich denke zwar, das es mit der
Row_T
Klasse zu tun hat, aber Ich hab keinen Plan, wo ich da ansetzen soll...
-
deine Funktionen
operator[]
sind nichtconst
.Bei
rows()
hast du z.B. korrekt einconst
da stehen:
std::size_t rows() const { return rows_; }
Bei demoperator[]
hast du das nicht gemacht.
-
Ja danke, und genau da verliere ich den Überblick.
Ich kann zwar der KlasseRow_T
hinzufügen:T operator[](const std::size_t column) const //neu { if (column >= cols_) throw std::out_of_range("operator[](): column: " + std::to_string(column)); return row_data[column]; } T& operator[](const std::size_t column) { if (column >= cols_) throw std::out_of_range("operator[](): column: " + std::to_string(column)); return row_data[column]; }
aber dann geht es hier weiter:
Row_T operator[](const std::size_t row) const { if (row >= rows_) throw std::out_of_range("operator[](): row: " + std::to_string(row)); return { &elements_[row * columns_], columns_ }; // hier bekomme ich dann Fehler } Row_T& operator[](const std::size_t row) { if (row >= rows_) throw std::out_of_range("operator[](): row: " + std::to_string(row)); return { &elements_[row * columns_], columns_ }; }
-
Dein const operator sollte auch
const T&
zurückgeben, wenn dein non-const operatorT&
zurückgibt.
-
Oh, hoffentlich habe ich richtig verstanden, aber richtig
const
würde dann so aussehen?:const Row_T& operator[](const std::size_t row) const { if (row >= rows_) throw std::out_of_range("operator[](): row: " + std::to_string(row)); return { &elements_[row * columns_], columns_ }; // hier dann error }
Falls ja, dann bekomme ich diese Fehlermeldung:
Error C2440 'return': cannot convert from 'initializer list' to 'const Matrix_T<int>::Row_T &'
Und dann weiß ich wieder nicht weiter
-
Da ich inzwischen meine, das auch eine rudimentäre Matrizen-Klasse für mein aktuelles Vorhaben overkilled ist, verschiebe ich dieses Thema erst mal und ändere die Klasse zu einem simplen
Array2D_T
ohneoperator[]
. Damit kann ich jetzt gut weitermachen.#pragma once #include <vector> #include <algorithm> #include <stdexcept> template <typename T> class Array2D_T { std::size_t rows_ = 0; std::size_t columns_ = 0; std::vector<T> elements_; public: Array2D_T() = default; Array2D_T(const std::size_t rows, const std::size_t columns, const T& v = {}) : rows_{ rows }, columns_{ columns }, elements_(rows * columns, v) {} Array2D_T(const std::initializer_list<std::initializer_list<T>>& elements) : Array2D_T{ elements.size(), std::max(elements, [](const auto& row, const auto& column) { return row.size() < column.size(); }).size() } { std::size_t r = 0; for (const auto& row : elements) { if (r >= rows_) break; std::size_t c = 0; for (const auto& v : row) { if (c >= columns_) break; (*this)(r, c) = v; ++c; } ++r; } } std::size_t rows() const { return rows_; } std::size_t columns() const { return columns_; } const T& operator()(const std::size_t row, const std::size_t column) const { const std::size_t idx = toLinearIndex(row, column); return elements_[idx]; } T& operator()(const std::size_t row, const std::size_t column) { const std::size_t idx = toLinearIndex(row, column); return elements_[idx]; } private: std::size_t toLinearIndex(const std::size_t row, const std::size_t column) const { if (row >= rows_) throw std::out_of_range("toLinearIndex(): row"); if (column >= columns_) throw std::out_of_range("toLinearIndex(): column"); return columns_ * row + column; } };
-
@zeropage Nur so als Anregung: Mit dem
operator()
statt desoperator[]
für den Elementzugriff ist das nicht nur deutlich angenehmer zu implementieren (ohne Gefummel mit Proxy-Objekten), sondern auch simpler in der Anwendung. Selbst auf ner US-Tastatur istm(i, j) = x
einfacher zu schreiben alsm[i][j] = x
. Und IMHO sogar leichter zu lesenEdit: Ach sorry, das haste ja für
Array2D_T
schon so gemacht. Ich sehe keinen Grund, weshalb man das für eine Matrixklasse nicht genau so machen könnte. Gibts da ne spezielle Motivation für?
-
Die Motivation war, die selbe Syntax wie in anderen Sprachen zu benutzen (also mit
[][]
), um leichter übersetzen zu können und natürlich es auch zu beherrschen. (Was noch nicht ganz geklappt hat )
-
Irgendwie stimmt da was an der Proxyklasse nicht. Bzw, an dem
[]
Operator der Matrix Klasse.Hast du mal Testcases geschrieben, für den verändernden Zugriff? Also, über die Referenz und dann geguckt ob die Veränderung auch in der Matrix ankommt?
Wenn ich das gerade richtig sehe, ruft dein Return in
Row_T& operator[]
den Konstruktor von Row_T auf. Und zwar innerhalb der Funktion und gibt darauf eine Referenz zurück. Das Proxyobjekt lebt dann aber nicht mehr.Ich würde zwei Proxy Klassen schreiben, denke ich. Einmel
Row_T
. Da kommt eine Kopie des Elements rein und eine Kopie davon wird von der Const Überladung zurück gegeben und ein einmalRow_T_Ref
da kommt eine Referenz auf das Vektorelement rein und eine Kopie von dem Typen wird von der Non const Überladung zurück gegeben.
-
Wie ich das sehe gibt du hier
const Row_T& operator[](const std::size_t row) const { if (row >= rows_) throw std::out_of_range("operator[](): row: " + std::to_string(row)); return { &elements_[row * columns_], columns_ }; // hier dann error }
ein const-Referenz auf ein lokales Objekt zurück, das geht so nicht.
Row_T
ist ein Leichtgewicht, du kannst hier ruhig return-by-value anwenden. Dem Aufrufer ist es dann überlassen, ob er mit einer Kopie oder einer const-Referenz arbeiten möchten. Die Benutzung als const-Referenz ist zulässig, weil der Standard das explizit erlaubt.Row_T operator[](const std::size_t row) const { if (row >= rows_) throw std::out_of_range("operator[](): row: " + std::to_string(row)); return { &elements_[row * columns_], columns_ }; // hier dann error } Row_T r1 = m[0]; // lokale Kopie, ok Row_T& r2 = m[0]; // non-const Referenz, nicht ok, weil sie nicht an ein temporary gebunden werden kann Row_T const& r2 = m[0]; // const Referenz, ok, weil sie an ein temporary gebunden werden kann, das garantiert der Standard
-
@zeropage sagte in Probleme mit eigener Matrix-Klasse:
Die Motivation war, die selbe Syntax wie in anderen Sprachen zu benutzen (also mit
[][]
), um leichter übersetzen zu können und natürlich es auch zu beherrschen. (Was noch nicht ganz geklappt hat )Hmm... ich kenne nicht viele andere Sprachen, in denen ich schonmal Matrix-Objekte verwendet habe. Aber selbst Matlab verwendet soweit ich mich erinnere für den Elementzugriff die
m(i, j)
-Syntax, wobei die Indizes dort auch noch Intervalle sein können. Will man irgendwann mal sowas haben, dann wird die[i][j]
-Syntax m.E. immer anstrengender zu lesen:m[range(a, b)][range(c, d)] = 0
vs.m(range(a, b), range(c, d)) = 0
. Kann aber auch nur Gewöhnungssache sein. Subjektiv würd ich die runden Klammern auf jeden Fall bevorzugen. Finde die eckigen außerhalb von simplen, eindimensionalen c-array-artigen Zugriffen sogar eher hässlich, da ich sie mit Lowlevel-C-Style assoziiere (und mehrdimensionale auch immer mit ineffizienten Pointer-auf-Pointer)
-
Schon mal danke, das Ihr Euch mit dem Kram auseinandersetzt Ich komm aber auch mit euren Hinweisen nicht weiter, ich muss wirklich sehen, wie das ausgeschrieben werden muss, um es hoffentlich zu verstehen. Diese Varianten hier führen immer zu dem selben Fehler: (natürlich nicht alle gleichzeitig im Code)
const Row_T& operator[](const std::size_t row) const { if (row >= rows_) throw std::out_of_range("operator[](): row"); return { &elements_[row * columns_], columns_ }; } Row_T& operator[](const std::size_t row) { if (row >= rows_) throw std::out_of_range("operator[](): row"); return { &elements_[row * columns_], columns_ }; } Row_T operator[](const std::size_t row) const { if (row >= rows_) throw std::out_of_range("operator[](): row"); return { &elements_[row * columns_], columns_ }; // Error C2440 'return': cannot convert from 'initializer list to 'Matrix_T<int>::Row_T' }
Ich will die Schreibweise mit
operator[]
auch gar nicht verteidigen oder wünschen, wollte nur wissen, was da nicht funktioniert. Wenn das aber so kompliziert ist, sollte ich es einfach lassen.
-
Dein
Row_T
hält einen non-constT* row_data
, der Compiler meckert, weil dasrow_data
bei der const-Konstruktion mit einem non-const Zeiger initialisiert werden soll.
-
Muss ich jetzt davon ausgehen, das diese ganze Konstruktion mit der Proxyklasse nicht lösbar ist?
-
@Finnegan sagte in Probleme mit eigener Matrix-Klasse:
@zeropage sagte in Probleme mit eigener Matrix-Klasse:
Die Motivation war, die selbe Syntax wie in anderen Sprachen zu benutzen (also mit
[][]
),Finde die eckigen außerhalb von simplen, eindimensionalen c-array-artigen Zugriffen sogar eher hässlich, da ich sie mit Lowlevel-C-Style assoziiere (und mehrdimensionale auch immer mit ineffizienten Pointer-auf-Pointer)
PS: zB Java-Code. Der benutzt die eckigen Klammern, da kann ich dann den ganzen Code kopieren und brauche nur noch den
private static
Kram ändern. Das reicht meist schon aus.edit: solange ich den nicht zu einer const-Methode machte, hatte dieses Vorgehen auch Erfolg. Buchstäblich seit gestern
-
@zeropage sagte in Probleme mit eigener Matrix-Klasse:
Muss ich jetzt davon ausgehen, das diese ganze Konstruktion mit der Proxyklasse nicht lösbar ist?
Ist schon lösbar, ich denke aber du brauchst noch einen
Const_Row_T
dafür mit einemconst T* row_data
Member anstatt eines non-const.Weniger redundant geht's, wenn man
Row_T
zu einer Template-Klasse macht:template <typename T> class Matrix_T { ... template <typename U> class Row_T { U* row_data; ... U& operator[](const std::size_t column) const { return row_data[column]; } ... } ... Row_T<T> operator[](const std::size_t); Row_T<const T> operator[](const std::size_t) const; ... }
Nicht getestet, sollte vom Prinzip her aber so funktionieren.
Das eigentliche Problem ist
const T& std::vector<T>::operator[](size_type pos) const
. Die Funktion wird aufgerufen, wenn du deinRow_T
initialisierst. Die gibt einconst T&
zurück, und damit kannst du natürlich keinen Zeiger auf nicht-konstanteT
initialisieren.std::vector<T>::operator[]
könnte durchaus auch einT&
zurückgeben und trotzdem const-qualifiziert sein - schliesslich verändert die Funktion den Vektor selbst nicht* - aber das ist eben so spezifiziert und macht auch für die intuitive Verwendung vonstd::vector
Sinn. Strikt erforderlich ist es aber nicht - auch wenn es gutes Design ist IMHO.In meinem Beispiel habe ich
Row_T<U>::operator[]
übrigens const-qualifiziert, auch wennU
ein nicht-const
-qualifiziertesT
sein könnte. Das geht, da die Funktion keinen Member vonRow_T<U>
mutiert. So muss man nur eine Funktion dafür schreiben. Für eine reine Proxy-Klasse wie diese hier geht das IMHO okay - fallsRow_T
auch für den Anwender instanzierbar sein soll, und nicht ausschliesslich für reine temporäre Proxy-Objekte, dann würde ich allerdings empfehlen, das auch so wiestd::vector
zu machen. Die Intuition würe mir als Anwender nämlich sagen, dass eineconst Row
eben das ist, eine unveränderbare Matrixzeile und dass ich da keine Elemente verändern darf.* Als
const std::vector<T>
wird der interneT*
zu einemT* const
, der Pointer selbst wird alsoconst
, nicht die Objekte, auf die er zeigt. Man könnte also durchaus aus einerconst
-qualifizierten Funktion eine nicht-const&
auf ein Element zurückgeben. Auch (nützlich für Templates und deduzierte Typen):U = T*
->const U = T* const
(und nichtconst T*
a.k.a.T const*
).
-
Also ich muss schon sagen, vielen Dank für diese ausführliche Antwort. Leider ist mir das doch alles etwas zu komplex. Vielleicht ist es nur temporär, aber zur Stunde bekomme ich auch mit dieser ausführlichen Antwort nix zum Laufen
Was zwar nicht allzu schlimm ist, weil in diesem Fall die Nutzung des
operator[]
ja wie Du auch schon geschrieben hast, mehr oder weniger Schnick-Schnack ist, wollte aber wenigstens auf die Antwort reagieren.
-
@zeropage sagte in Probleme mit eigener Matrix-Klasse:
Was zwar nicht allzu schlimm ist, weil in diesem Fall die Nutzung des
operator[]
ja wie Du auch schon geschrieben hast, mehr oder weniger Schnick-Schnack ist, wollte aber wenigstens auf die Antwort reagieren.Schnickschnack ist das nicht, aber
operator()
macht es IMHO deutlich simpler in Anwendung und Wartung.Auch hat dein akutes Problem hier weniger mit dem
operator[]
zu tun. Das passiert denke ich immer wenn man referenz-artige Objekte wieRow_T
zurückgibt. Da braucht es eben auch auch noch einenconst_reference
-Typen. Auch einoperator(i, j) const
muss einconst T&
zurückgeben, dastd::vector<T>::operator[]() const
eben einconst T&
liefert. Da hat man's ebenfalls mit zwei verscheidenen Typen zu tun,T&
undconst T&
.
-
Na gut, ok.
-
@Finnegan sagte in Probleme mit eigener Matrix-Klasse:
template <typename U> class Row_T { U* row_data; ... U& operator[](const std::size_t column) const { return row_data[column]; } ... }
Besser
const U& operator[](const std::size_t column) const { return row_data[column]; } U& operator[](const std::size_t column) { return row_data[column]; }
Oder?