Probleme mit eigener Matrix-Klasse



  • 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?



  • @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 wie std::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. Im const Matrix_T-Fall wird Row_T<const T> zurückgegeben und U& ist bereits eine konstante Referenz (const T&). Die Aussage der const-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 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 irgendwie const-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* und std::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 habe Row und ConstRow 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 und ConstRow um einen Increment erweitert lassen sie sich auch als Column und ConstColumn verwenden, da musste halt für den operator[]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 wie std::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 irgendwie const-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.


Anmelden zum Antworten