Probleme mit eigener Matrix-Klasse
-
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.
-
@hustbaer sagte in Probleme mit eigener Matrix-Klasse:
@Swordfish Ich hab mich am
const std::size_t column
orientiert. Ansonsten natürlich immer east const.Mir ist das ehrlich gesagt ziemlich egal, hauptsache man einigt sich auf eins und zieht es dann konsequent durch. Ich verwende West.
Was ich allerdings erstaunlich bei dem Blog-Eintrag in deiner Signatur finde, ist dass ich danach irgendwie mehr von west-const überzeugt war, als von east-const. Der Autor schreibt zwar, die Argumente für west-const seien nicht sonderlich stark, aber ein Knüllerargument für east-const konnte ich irgendwie nicht finden. Nach der Lektüre ist es mir immer noch ziemlich wumpe. Wenn das wirklich ne Revolution werden soll, dann ist mir das nicht polarisierend genug
-
@Finnegan sagte in Probleme mit eigener Matrix-Klasse:
@hustbaer sagte in Probleme mit eigener Matrix-Klasse:
@Swordfish Ich hab mich am
const std::size_t column
orientiert. Ansonsten natürlich immer east const.Mir ist das ehrlich gesagt ziemlich egal, hauptsache man einigt sich auf eins und zieht es dann konsequent durch. Ich verwende West.
This!
Was ich allerdings erstaunlich bei dem Blog-Eintrag in deiner Signatur finde, ist dass ich danach irgendwie mehr von west-const überzeugt war, als von east-const. Der Autor schreibt zwar, die Argumente für west-const seien nicht sonderlich stark, aber ein Knüllerargument für east-const konnte ich irgendwie nicht finden.
Vor allem steht da bei dem einem Beispiel "These declarations are harder to read when the West const notation is used.", während ich es genau umgekehrt empfinde. Gerade bei Pointern bevorzuge ich "const möglichst weit auseinander", damit man da nichts verwechselt. Bei east const ist meine erste Assoziation immer: "Aha, nur der Pointer konstant. Moment mal. Ach nee, dann müsste das const ja noch weiter rechts stehen."
Aber wahrscheinlich alles Gewöhnungs- und Geschmackssache. Warum ich west nutze: weil die east-const Leute gefühlt immer behaupten, dass ihre Präferenz die einzig wahre wäre. Beispiel hier von @Swordfish: nichts zum eigentlichen Thema, aber hier wieder mal "west ist nicht schön" einbringen. Das erzeugt bei mir nur noch mehr Trotz, da "schön" subjektiv ist und ich noch kein überzeugendes Argument gesehen habe.
-
lol
Dass man es nicht konsequent durchziehen kann ist doch gerade das Argument gegen const west. Ich meine... zeigt mir mal bitte wie man mit konsequentem const west nen konstanten Zeiger auf nen Integer schreibt.
ps: Ja, Firefox ist auch der bessere Browser
-
@hustbaer sagte in Probleme mit eigener Matrix-Klasse:
Ja, Firefox ist auch der bessere Browser
@wob sagte in Probleme mit eigener Matrix-Klasse:
Beispiel hier von @Swordfish: nichts zum eigentlichen Thema, aber hier wieder mal "west ist nicht schön" einbringen. Das erzeugt bei mir nur noch mehr Trotz, da "schön" subjektiv ist und ich noch kein überzeugendes Argument gesehen habe.
Das war nur für Husti, weil ich weiß daß er /eigentlich/ ein east-constler ist. Musst dich nicht gestichelt fühlen.
-
@hustbaer sagte in Probleme mit eigener Matrix-Klasse:
[...] zeigt mir mal bitte wie man mit konsequentem const west nen konstanten Zeiger auf nen Integer schreibt.
Da man einen konstanten Zeiger ohnehin mit einem anderen Zeiger initialisieren muss, um den sinnvoll verwenden zu können*:
int value; const auto pointer = &value;
... oder für Hardware-Bastler:
const auto pointer = reinterpret_cast<int*>(0xdeadbeef);
Für eine via Member Initializer List initialisierte
const
Member-Variable, die hab ich aber leider keine gute Lösung anzubieten.Allerdings dürfte man nen konstanten Pointer doch eher extrem selten direkt als Typ dekrarieren müssen. Die treten denke ich ungleich häufiger z.B. als (non-
const
-deklarierte) Member vonconst Class
oder als Template-Parameterconst T
(mit z.B.T = int*
) auf.Aber nichtsdestotrotz ist es mir immer noch egal. Ich kann ohne Schmerzen auch mit east-const arbeiten. Würd aber nicht gut zu meinem anderen Code passen - insofern ist das Argument der bestehenden Codebases schon ziemlich stark
*gibts nen Anwendungsfall für nen uninitalisierten
const
-Garbage-Wert?
-
Danke für die Codes.
@DocShoeIn Deinem Code habe ich an dieser Stelle folgenden Fehler:
const_row_type operator[](size_type index) const { const_pointer beg = Elements_.data() + index * Cols_; return const_row_type(beg, beg + Cols_); // Error C3878 syntax error: unexpected token 'return' following 'id_expression' }
Wenn ich nach dem Fehler suche, kommt nur "Hoppla! Es wurde keine F1-Hilfe gefunden." Da ich aber seit Visual Studio 22 mit C++20 öfters Fehler bekomme, die es unter VS19 mit C++17 nicht gab, habe ich den Code damit versucht zu kompilieren, und dann kommt nur ein lapidares
C2760 syntax error: unexpected token 'return', expected ';'
wofür es aber immerhin eine Hilfe gibt, die mir aber nicht weiterhilft.
Zu erwähnen wäre vielleicht noch, das die Variablen in dieser Funktion nicht farblich gekennzeichnet werden, als ob der Compiler diese nicht zuordnen kann. Beirow_type operator[](size_type index) { pointer beg = Elements_.data() + index * Cols_; return row_type(beg, beg + Cols_); }
macht er noch die farbliche Kennzeichnung.
edit: So langsam habe ich das Gefühl, das man für die Nutzung von eckigen Klammern in Arrays jenseits der STL seine Doktorarbeit schreiben kann
-
Jo, da waren noch ein paar Fehler drin, hab das gefixt und hochgeladen.
Besonders nervig ist da ein unsichtbares Zeichen in Zeile 52, keine Ahnung, wo das herkam.
-
Wunderbar! Vielen Dank.
Ich habe noch den Konstruktor mit initializer-list hinzugefügtMatrix2D(const std::initializer_list<std::initializer_list<T>>& elements) : Matrix2D{ elements.size(), std::max(elements, [](const auto& row, const auto& column) { return row.size() < column.size(); }).size() } { size_type r = 0; for (const auto& row : elements) { if (r >= Rows_) break; size_type c = 0; for (const auto& val : row) { if (c >= Cols_) break; (*this)[r][c] = val; ++c; } ++r; } }
Und damit funktioniert auch mein Test-Case (wenn ich das so nennen darf). Hoffe nur, das ich damit nicht einen weiteren Fehler fabriziert habe. Aber scheint ja alles gut zu sein.
#include "Matrix.h" #include <iostream> class Mat { Matrix2D<int> matrix; public: Mat() : matrix{ { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } } {} void printVal(const std::size_t r, const std::size_t c) const { std::cout << matrix[r][c] << '\n'; } void writeVal(const std::size_t r, const std::size_t c, const int val) { matrix[r][c] = val; } }; int main() { Mat mat; mat.printVal(0, 0); mat.writeVal(0, 0, 9); mat.printVal(0, 0); }