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 nicht const.

    Bei rows() hast du z.B. korrekt ein const da stehen:
    std::size_t rows() const { return rows_; }
    Bei dem operator[] hast du das nicht gemacht.



  • Ja danke, und genau da verliere ich den Überblick.
    Ich kann zwar der Klasse Row_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 operator T& 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 ohne operator[]. 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 des operator[] 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 ist m(i, j) = x einfacher zu schreiben als m[i][j] = x. Und IMHO sogar leichter zu lesen 😉

    Edit: 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 einmal Row_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-const T* row_data, der Compiler meckert, weil das row_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 einem const 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 dein Row_T initialisierst. Die gibt ein const T& zurück, und damit kannst du natürlich keinen Zeiger auf nicht-konstante T initialisieren. std::vector<T>::operator[] könnte durchaus auch ein T& 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 von std::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 wenn U ein nicht-const-qualifiziertes T sein könnte. Das geht, da die Funktion keinen Member von Row_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 - 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 wie std::vector zu machen. Die Intuition würe mir als Anwender nämlich sagen, dass eine const Row eben das ist, eine unveränderbare Matrixzeile und dass ich da keine Elemente verändern darf.

    * Alsconst std::vector<T> wird der interne T* zu einem T* const, der Pointer selbst wird also const, nicht die Objekte, auf die er zeigt. Man könnte also durchaus aus einer const-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 nicht const 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 wie Row_T zurückgibt. Da braucht es eben auch auch noch einen const_reference-Typen. Auch ein operator(i, j) const muss ein const T& zurückgeben, da std::vector<T>::operator[]() consteben ein const T& liefert. Da hat man's ebenfalls mit zwei verscheidenen Typen zu tun, T& und const 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?


Anmelden zum Antworten