Probleme mit eigener Matrix-Klasse
-
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?
-
@hustbaer sagte in Probleme mit eigener Matrix-Klasse:
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?
Ja. Aber noch mehr Code zu schreiben und zu pflegen. Ich hatte ja auch dazu geschrieben:
Für eine reine Proxy-Klasse wie diese hier geht das IMHO okay - falls
Row_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.Man kann sich das meiner Meinung nach für so ein ausschließliches Proxy-Objekt durchaus erlauben, wenn es ohnehin nicht
const
-qualifiziert verwendet werden soll. Imconst Matrix_T
-Fall wirdRow_T<const T>
zurückgegeben undU&
ist bereits eine konstante Referenz (const T&
). Die Aussage derconst
-Qualifizierung für den Operator ist bei mir absolut lowlevel: Die Funktion verändert keinen Member des Objekts (der gehaltene Pointer wird nicht umgebogen oder sowas). Deine Variante ist eine erweiterteconst
-Bedeutung. Nicht strikt notwendig, aber sinnvoll aus "least-surprise"-Gründen. Allderdigs nur, wenn vorgesehen ist, dass der User das Ding tatsächlich irgendwieconst
-qualifiziert instanziert oder referenziert - sonst ist das "Dead Code"Wenn das eine user-instanzierbare Klasse werden soll, dann natürlich besser deine Variante.
P.S.: Auch interessant:
std::vector<T>::data() const -> const T*
undstd::unique_ptr<T>::get() const -> T*
. Zwei Beispiele an denen man sieht, dass das manchmal eine Design-Entscheidung ist. Man kann ja durchaus eine Weltanschauung haben, mit der man beide Klassen für fast dasselbe hält
-
@zeropage:
Hier haste mal was zum Spielen. Ich habeRow
undConstRow
als eigenständige Klassen implementiert, das kann man bestimmt noch iwie geschickter lösen, ohne endlos viel Code zu duplizieren. Ansonsten haben die beiden ziemlich viel boiler plate code, und da ist noch viel Luft nach oben ( z.B.cbegin()/cend(), rbegin()/rend(), crebgin()/crend(), front(), back()
und die ganzen anderen Komfortfunktionen:#include <vector> #include <algorithm> #include <type_traits> template<typename T> class Row; template<typename T> class ConstRow; template<typename T> class Matrix2D { // Matrix aus bool nicht erlauben static_assert( !std::is_same<bool,T>::value, "T must not be bool because of std::vector<bool> anomaly" ); public: using value_type = T; using reference = T&; using const_reference = T const&; using pointer = T*; using const_pointer = T const*; using size_type = std::size_t; using difference_type = std::ptrdiff_t; using row_type = Row<T>; using const_row_type = ConstRow<T>; private: std::vector<T> Elements_; size_type Rows_ = 0; size_type Cols_ = 0; public: Matrix2D() = default; Matrix2D( size_type rows, size_type cols ) : Matrix2D( row, cols, value_type() ) { } Matrix2D(size_type rows, size_type cols, const_reference v ) : Elements_( rows * cols, v ), Rows_( rows ), Cols_( cols ) { } row_type operator[]( size_type index ) { pointer beg = Elements_.data() + index * Cols_; return row_type( beg, beg + Cols_ ); } const_row_type operator[]( size_type index ) const { const_pointer beg = Elements_.data() + index * Cols_; return const_row_type( beg, beg + Cols_ ); } }; template<typename T> class Row { T* Begin_= nullptr; T* End_ = nullptr; public: Row( T* beg, T* end ) : Begin_( beg ), End_( end ) { } bool empty() const { return Begin_ == End_; } std::size_t size() const { return std::distance( begin(), end() ); } T& operator[]( std::size_t index ) { return Begin_[index]; } T const& operator[]( std::size_t index ) const { return Begin_[index]; } T* begin() { return Begin_; } T* end() { return End_; } T const* begin() const { return Begin_; } T const* end() const { return End_; } }; template<typename T> class ConstRow { T const* Begin_ = nullptr; T const* End_ = nullptr; public: ConstRow( T const* beg, T const* end ) : Begin_( beg ), End_( end ) { } ConstRow( Row<T> const& r ) : Begin_( r.begin() ), End_( r.end() ) { } T const& operator[]( std::size_t index ) const { return Begin_[index]; } }; int main() { Matrix2D m( 2, 2,-1 ); m[0][0] = 1; }
Wenn man
Row
undConstRow
um einen Increment erweitert lassen sie sich auch alsColumn
undConstColumn
verwenden, da musste halt für denoperator[]
etwas rumrechnen.
-
@Finnegan sagte in Probleme mit eigener Matrix-Klasse:
Für eine reine Proxy-Klasse wie diese hier geht das IMHO okay - falls
Row_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.Oops, sorry. Hab nicht den ganzen Thread gelesen
Deine Variante ist eine erweiterte
const
-Bedeutung. Nicht strikt notwendig, aber sinnvoll aus "least-surprise"-Gründen. Allderdigs nur, wenn vorgesehen ist, dass der User das Ding tatsächlich irgendwieconst
-qualifiziert instanziert oder referenziert - sonst ist das "Dead Code"Ja. Aber ist recht wenig dead code. Das wäre IMO vertretbar.
Wobei mir auffällt... damit das wirklich Sinn macht, müsste man die Proxy-Klasse non-copyable machen. Sonst kann sich ja jeder ne Kopie ziehen, die ist dann nicht mehr const, und dann kann man über die Kopie zugreifen. Und non-copyable hat auch wieder Nachteile. Also vermutlich eh besser ohne den const-Overload im Proxy.
Nicht alles was ich schreibe ist immer 100% durchdacht
-
@hustbaer Noch east-const und es ist schön ;p
-
@Swordfish Ich hab mich am
const std::size_t column
orientiert. Ansonsten natürlich immer east const.