einfache Matrizenklasse, Vererbung



  • Hallo,
    ich schreibe gerade an einer einfachen Matrizenklasse, nur für eine einfache Anwendung, und bekomme doch langsam Schwierigkeiten. Die alte Version hat leidlich funktioniert, doch beim "ins Saubere schreiben" ist das nicht mehr so einfach.

    Ich hab eine Klasse, die sich nur um die Matrix selbst kümmern soll (sie war ursprünglich dafür gedacht, sich nur um das Array zu kümmern, die ist dann etwas größer geworden) und eine, die nur die Rechen-Operatoren hat. Und ein Beispiel.

    Die Array-Klasse

    using floatT = float;
    using floatVec = std::vector<float>;
    using floatArray = std::vector<std::vector<float>>;
    
    class Matrix
    {
    public:
    	Matrix() {}
    	~Matrix() {}
    	Matrix(const Matrix& mx);
    	void operator=(const Matrix& mx) { copyMatrix(mx); }
    
    	Matrix(const std::size_t rows, const std::size_t columns);
    	Matrix(const std::size_t rows, const std::size_t columns, const floatArray& fA);
    	Matrix(const floatArray& fA);
    
    	void resizeMatrix(const std::size_t rows, const std::size_t columns);
    	void fillMatrix(const floatT& fT);
    	void fillMatrix(const floatArray& fA);
    	void setElement(const std::size_t row, const std::size_t column, const float v);
    	void setDV(const float v);
    
    	float element(const std::size_t row, const std::size_t column) const;
    	floatVec row(const std::size_t r) const;
    	floatVec column(const std::size_t c) const;
    	std::size_t rows() const;
    	std::size_t columns() const;
    	std::size_t unitSize() const;
    	std::size_t size() const;
    	float dv() const;
    	floatArray toArray(const floatVec& fV) const;
    	floatArray array() const;
    	bool operator == (const floatT& fT) const;
    	bool operator != (const floatT& fT) const;
    	bool sameDimension(const Matrix& mx) const;
    	bool operator == (const Matrix& mx) const;
    	bool operator != (const Matrix& mx) const;
    
    	void printElement(const std::size_t row, const std::size_t column, std::ostream& stream = std::cout) const;
    	void printRow(const std::size_t row, std::ostream& stream = std::cout) const;
    	void printColumn(const std::size_t column, std::ostream& stream = std::cout) const;
    	void print(const std::string& name = {}, std::ostream& stream = std::cout) const;
    
    protected:
    	void copyMatrix(const Matrix& mx);
    
    private:
    	std::size_t rows_ = 0;
    	std::size_t columns_ = 0;
    	std::size_t unitSize_ = 0;
    	floatArray array_;
    	float dv_ = 666.; //testweise
    
    	void createArray(const float dv);
    	void constructDiagonal(const float dv);
    };
    

    Die Operatoren-Klasse

    class Mat : public Matrix
    {
    public:
    	Mat() {}
    	~Mat() {}
    	Mat(const Mat& mt) : Matrix(mt) {}
    	void operator=(const Mat& mt) { copyMatrix(mt); }
                                
    	Mat(const std::size_t rows, const std::size_t columns) :                           Matrix(rows, columns) {}
    	Mat(const std::size_t rows, const std::size_t columns, const floatArray& fArray) : Matrix(rows, columns, fArray) {}
    	Mat(const floatArray& fArray) :                                                    Matrix(fArray) {}
    
    
    	void operator+=(const float f);
    	void operator-=(const float f);
    	void operator*=(const float f);
    	void operator/=(const float f);
    	void operator+=(const Mat& mt);
    	void operator-=(const Mat& mt);
    	Mat operator+(const float f) const;
    	Mat operator-(const float f) const;
    	Mat operator*(const float f) const;
    	Mat operator/(const float f) const;
    	Mat operator+(const Mat& mt) const;
    	Mat operator-(const Mat& mt) const;
    	Mat operator*(const Mat& mt) const;
    };
    

    Ob die so funktioniert, habe ich noch nicht getestet (die Operatoren selbst funktionieren aber schon), weil vorher ein Problem aufgetaucht ist, weshalb ich hier frage. Am folgenden Beispiel:

    class DMat : public Mat  //soll eine Diagonal Matrix sein, deshalb D
    {
    public:
    	DMat() {}
    	~DMat() {}
    	DMat(const DMat& mt) : Mat(mt) {}
    	void operator=(const DMat& mt) { copyMatrix(mt); }
    
    	DMat(const std::size_t size) : size_(size), Mat(size_, size_) 
    	{
    		resize(size_); //nur dann wird ein Array erstellt
    	}
    
    	void resize(const std::size_t size)
    	{
    		size_ = size;
    		resizeMatrix(size_, size_);
    	}
    
    	std::size_t size() const { return size_; }
    	float dv() const { return dv_;  }
    
    private:
    	std::size_t size_ = 0;
    	float dv_ = 666.; //testweise
    };
    

    Nun werden ohne resize(size_) keine Werte übergeben, das Array ist leer, ein Objekt muss doch aber erstellt worden sein, denn ich kann damit die Funktionen der class Matrix aufrufen?

    Ich könnte jetzt noch allerhand versuchen, aber vielleicht ist die Vererbung ganz falsch gelaufen, weshalb es so auch gar nicht richtig funktionieren kann? Denn ich stoße noch auch andere Sachen, die aber vielleicht den selben Grund haben.
    Also zusammengefasst, warum

    Mat mt(3, 3); //korrekt arbeitet
    DMat ut(3);   //das aber nicht
    


  • DMat(const std::size_t size) : size_(size), Mat(size_, size_) 
    

    Die Reihenfolge der Initialisierung wird nicht durch die Reihenfolge in der Initialisierungsliste bestimmt, hier wird Mat vor size_ initialisiert, dabei wird aber size_ verwendet.

    size_ scheint mir aber auch völlig überflüssig zu sein.



  • Dieses Design wird dir jede Menge Ärger machen. Beispiel:

    DMat diag(3);
    diag.resizeMatrix(1,5);
    // und jetzt eine Funktion aufrufen, die davon ausgeht, dass diag quadratisch ist...
    

    Siehe Liskov Substitution Principle. Im Zweifel lieber keine öffentliche Vererbung benutzen, wir leben nicht mehr in den 90ern.

    Es gäbe sicher noch mehr anzumerken, z.B. wieso deine Zuweisungoperatoren entgegen der Konvention void zurückgeben.



  • Danke für die beiden Antworten. Ja, die Reihenfolge in der Initialisierung kannte ich nicht und size_ ist dort tatsächlich überflüssig. EDIT: Und nicht nur dort...

    class DMat : public Mat
    {
    public:
    	DMat() {}
    	~DMat() {}
    	DMat(const DMat& mt) : Mat(mt) {}
    	DMat operator=(const DMat& mt) { copyMatrix(mt); }
    	DMat(const std::size_t size) : Mat(size, size){}
    
    	void resize(const std::size_t size)
    	{
    		resizeMatrix(size, size);
    	}
    
    	float dv() const { return dv_;  }
    
    private:
    	float dv_ = 1.; 
    };
    

    void operator() ist wirklich blöd. Kam mir schon ein wenig komisch vor. Danke.
    Wegen dem Prinzip müsste ich jetzt mehr umschauen. resizeMatrix() ist jetzt protected, Bevor ich mich jetzt zu dem Prinzip umschaue, mit 'öffentliche Vererbung' ist die public- Ableitung gemeint?

    EDIT: jetzt protected gemacht 😉



  • @zeropage sagte in einfache Matrizenklasse, Vererbung:

    Wegen dem Prinzip müsste ich jetzt mehr umschauen. resizeMatrix() ist jetzt protected

    Ganz andere Frage am Rande: Hast du eigentlich einen konkreten Anwendungsfall für resizeMatrix()? So aus dem Bauch heraus würde ich erstmal vermuten, dass sich die Fälle, wo eine Matrix ein neue Größe bekommt, sauberer durch Kopieren in eine neue Matrix umsetzen lassen.

    Beispielsweise wird man bei der Multiplikation nicht-quadratischer Matrizen (wo die resultierende Matrix ja eine andere Größe hat) das Ergebnis ohnehin in eine neue Matrix schreiben. Diese kann man dann gleich mit der richtigen Größe anlegen.

    resize rauszuwerfen und vielleicht sogar rows_ und columns_ konstant zu machen würde das Problem nämlich auch lösen. Nur meine persönliche Meinung (als jemand, der sich am produktivsten fühlt, wenn er am Ende eines Arbeitstags eine negative Anzahl Codezeilen produziert hat 😉 )

    Weitere Anmerkungen:

    std::vector<std::vector<float>> ist nicht sonderlich effizient, da die Matrixelemente dadurch nicht zusammenhängend im Speicher liegen. Besser das in einen std::vector<float>{ rows_ * columns_ }zu packen und die Indizes selbst zu berechnen (z.B. i * columns_ + j bei Row-major-Anordnung).

    Zweistellige Operatoren wie Mat operator*(const float f) implementiert man besser als freie Funktionen. Das hat u.a. den Vorteil, dass der Mat-Typ nicht immer nur auf der linken Seite der Operation vorkommen darf:

    Mat operator*(const Mat& m, const float f);
    Mat operator*(const float f, const Mat& m);
    

    Diese freien Operatoren würden sowohl m * f wie auch f * m erlauben. Letzteres ist nicht möglich, wenn man das nur als Member-Funktion macht.



  • Danke, sind zwar erst ein paar Minuten vergangen, aber ich kann trotzdem schon antworten. resizeMatrix() hat tatsächlich keinen Anwendungszweck. Das kam in einer früheren Version vor, hatte sich dann aber erledigt und mitgeschleppt.

    std::vector<float>{ rows_ * columns_ } hatte ich zuerst auch, bin aber im Zuge der Array-Klasse umgeschwenkt. Werde ich wieder ändern.

    Von den freien Funktionen hatte ich schon gelesen, konnte damit aber nicht umgehen. Mit den freien Operatoren wird mir das aber klar. Danke nochmals.



  • @zeropage sagte in einfache Matrizenklasse, Vererbung:

    Danke, sind zwar erst ein paar Minuten vergangen, aber ich kann trotzdem schon antworten. resizeMatrix() hat tatsächlich keinen Anwendungszweck. Das kam in einer früheren Version vor, hatte sich dann aber erledigt und mitgeschleppt.

    Darauf noch ein Gute-Nacht-Zitat zu später Stunde:
    "Vollkommenheit entsteht offensichtlich nicht dann, wenn man nichts mehr hinzuzufügen hat, sondern wenn man nichts mehr wegnehmen kann." - Antoine de Saint-Exupéry



  • @Finnegan sagte in einfache Matrizenklasse, Vererbung:

    Weitere Anmerkungen:
    std::vector<std::vector<float>> ist nicht sonderlich effizient, da die Matrixelemente dadurch nicht zusammenhängend im Speicher liegen. Besser das in einen std::vector<float>{ rows_ * columns_ }zu packen und die Indizes selbst zu berechnen (z.B. i * columns_ + j bei Row-major-Anordnung).

    Ist mir auch aufgefallen: using floatArray = std::vector<std::vector<float>>;

    Ein Vorteil wäre, dass unterschiedliche Spaltenanzahlen je Reihe möglich wären. Aber von solchen Matrizen habe ich noch nie gehört.



  • Am schlimmsten finde ich da allerdings den Namen, denn unter einem floatArray verstehe ich ein Array von Floats, nicht einen std::vector eines std::vectors.



  • @wob Zu seiner Verteidigung: Ein vector ist auch nur ein aufgebohrtes Array. Immerhin muss der Inhalt genauso in einem zusammenhängenden Speicherbereich liegen, wie ein array einer ist.



  • @wob Wie findets du dynArray (Abkürzung für (Runtime-) Dynamic Array).



  • @Tyrdal sagte in einfache Matrizenklasse, Vererbung:

    mmerhin muss der Inhalt genauso in einem zusammenhängenden Speicherbereich liegen, wie ein array einer ist.

    std::vector<std::vector<float>> ist da aber ganz anders als float x[10][10], d.h die Speicherbereich hängen eben nicht zusammen.



  • @Tyrdal sagte in einfache Matrizenklasse, Vererbung:

    Zu seiner Verteidigung: Ein vector ist auch nur ein aufgebohrtes Array.

    Darum ging es doch nicht. Zu using floatArray = std::vector<float> hätte ich nichts gesagt. Nur es ist hier eben ein Array eines Arrays (bzw vector eines vectors).

    @titan99_

    Wie findets du dynArray (Abkürzung für (Runtime-) Dynamic Array).

    Tja, der Name passt. Aber warum braucht man das? Das ist auch gar nicht mein Punkt gewesen.



  • @wob Ah ok, gut vector von vector benutz ich eh nie, weil es keinen Sinn macht. Daher hab ich das wohl übersehen.



  • @titan99_ sagte in einfache Matrizenklasse, Vererbung:

    Ein Vorteil wäre, dass unterschiedliche Spaltenanzahlen je Reihe möglich wären. Aber von solchen Matrizen habe ich noch nie gehört.

    Ja, da habe ich dann, wenn nichts anderes vorgegeben, den ersten Vector als Spaltenanzahl genommen und sonst auch gar nicht auf Größe geachtet. Wenn sie reingepasst haben, wurden sie eingefügt, wenn nicht wurde der Vector abgeschnitten.

    Ich werde das aber wie gesagt, wieder zu std::vector<float> elements;ändern, wie ich es schon hatte. Gefällt mir auch besser.

    Edit: Als eine Übergabe werde ich weiterhin einen std::vector<std::vector<float>>anbieten und nach dem obigen Prinzip einfügen. Um zu Testzwecken schnell Daten übergeben zu können, finde ich den praktisch.



  • @zeropage sagte in einfache Matrizenklasse, Vererbung:

    Um zu Testzwecken schnell Daten übergeben zu können, finde ich den praktisch.

    Wie soll das praktisch sein?



  • Na, zum Beispiel

    std::vector<std::vector<float>> vArray;
    
    std::vector<float> f0 = { 1., 2., 3. };
    vArray.push_back(f0);
    std::vector<float> f1 = { 4., 5., 6. };
    vArray.push_back(f1);
    std::vector<float> f2 = { 7., 8., 9. };
    vArray.push_back(f2);
    std::vector<float> f3 = { .1, .2, .3 };
    vArray.push_back(f3);
    
    Mat mt(vArray);
    mt.print("mt");
    

    Wie würdest Du denn das machen?



  • std::vector<float> vArray{ 1., 2., 3.,
                               4., 5., 6.,
                               7., 8., 9.,
                               .1, .2, .3 };
    


  • Ok, einen std::vector<>weniger geschrieben. Für was? Der Doppelvector ist doch nicht die einzige Möglichkeit zur Übergabe, nur eine von anderen.



  • std::copy

    std::vector<float> vArray;
    
    std::vector<float> f0 = { 1., 2., 3. };
    std::copy(f0.begin(), f0.end(), vArray.end())
    std::vector<float> f1 = { 4., 5., 6. };
    std::copy(f1.begin(), f1.end(), vArray.end())
    std::vector<float> f2 = { 7., 8., 9. };
    std::copy(f2.begin(), f2.end(), vArray.end())
    std::vector<float> f3 = { .1, .2, .3 };
    std::copy(f3.begin(), f3.end(), vArray.end())
    

Log in to reply