Zuweisung an Referenz auf Basisklasse



  • Hallo allerseits.

    Ich verstehe hier etwas zum Thema Vererbung und Referenzen nicht und hoffe jemand kann hier helfen.

    Ich habe eine Basisklasse DataArray davon abgeleitet gibt es Vector und Matrix. Ich muss Daten rekursiv auswerten, das heißt aus einem DataArray wird ein neues DataArray berechnet, wobei der genaue Typ (Matrix oder Vector) auch mal wechseln kann. Probleme habe ich in folgender Methode:

    void FlatteningLayer::forward_propagate(DataArray& x_) const
    {  
      Matrix& x = dynamic_cast<Matrix&>(x_);
      x_ = x.flatten();
    }
    

    wobei die Operation flatten folgende Signatur hat:

    Vector Matrix::flatten() const;
    

    Ich erwarte, dass die Referenz x_ auf eine Matrix gecastet wird (das funktioniert), daraus einen Vektor berechnet (das funktioniert) und der Referenz x_ zuweist und ich diese später auch auf Vector casten kann. Hier ist das Problem, dass x_ aber weiterhin vom Typ Matrix ist, allerdings kann ich im Debugger auch nicht mehr auf die Matrix-spezifischen Attribute zugreifen. Die Zeile x_ = x.flatten() ist offensichtlich kompletter Murks. Ich weiß nicht mal warum diese Zeile überhaupt kompiliert. Einen passenden Assignment-Operator für Matrix aus DataArray oder Vector habe ich nicht implementiert. Vermutlich wird hier irgendein automatisch generierter Copy-Assignment-Operator für DataArray aufgerufen.

    Eine Lösung wäre mit Pointern zu arbeiten:

    void FlatteningLayer::forward_propagate(DataArray* x_) const
    {  
      Matrix* x = dynamic_cast<Matrix*>(x_);
      x_ = new Vector(x->flatten());
    
      // jaja, hier muss irgendwie noch ein delete hin
    }
    

    Pointer wollte ich hier aber eigentlich vermeiden. Geht es irgendwie mit Referenzen?

    Danke schonmal und viele Grüße.



  • @Max3000 sagte in Zuweisung an Referenz auf Basisklasse:

    Probleme habe ich in folgender Methode:
    void FlatteningLayer::forward_propagate(DataArray& x_) const

    Warum nimmt diese Funktion denn ein DataArray entgegen, wenn die aber in Wirklichkeit eine Matrix als Parameter benötigt?

    Ich erwarte, dass die Referenz x_ auf eine Matrix gecastet wird (das funktioniert), daraus einen Vektor berechnet (das funktioniert)

    so weit, so gut

    und der Referenz x_ zuweist und ich diese später auch auf Vector casten kann.

    Warum sollte das gehen? Die Anforderung ist doch, dass x_ ein DataArray& ist (deklarierter Typ). Der Vector könnte doch eine völlig andere Größe im Speicher haben. So eine Zuweisung kann doch nicht den Typ ändern.

    Die Zeile x_ = x.flatten() ist offensichtlich kompletter Murks. Ich weiß nicht mal warum diese Zeile überhaupt kompiliert.

    Warum? Ein Vector ist ein DataArray. Du kannst die also zuweisen. Alle Attribute, die der Vector zusätzlich zum DataArray hat, werden dabei ignoriert. Das nennt sich "slicing".

    Du scheinst zu versuchen, den Typ einer Variablen verändern zu wollen. Das geht nicht.

    Anstelle das irgendwie zu fixen: wäre es für dich nicht sinnvoller, wenn du lieber überall mit const& arbeiten würdest und den neuen Wert als Return-Value zurückgeben würdest anstelle von inplace-Änderungen?



  • Hallo.

    @wob sagte in Zuweisung an Referenz auf Basisklasse:

    Warum nimmt diese Funktion denn ein DataArray entgegen, wenn die aber in Wirklichkeit eine Matrix als Parameter benötigt?

    Es gibt neben FlatteningLayer noch weitere Klassen, die die gleiche Methode implementieren, die aber einen Vector als Input benötigen. Darum der Cast am Anfang.

    @wob sagte in Zuweisung an Referenz auf Basisklasse:

    Warum sollte das gehen? Die Anforderung ist doch, dass x_ ein DataArray& ist (deklarierter Typ). Der Vector könnte doch eine völlig andere Größe im Speicher haben. So eine Zuweisung kann doch nicht den Typ ändern.

    Ja, da hast du wohl recht. Ich denke ich versuche eine Referenz zu reassignen, was anscheinend nicht funktionieren darf.

    @wob sagte in Zuweisung an Referenz auf Basisklasse:

    Warum? Ein Vector ist ein DataArray. Du kannst die also zuweisen. Alle Attribute, die der Vector zusätzlich zum DataArray hat, werden dabei ignoriert. Das nennt sich "slicing".

    OK, das sehe ich ein. Dann wird der automatisch generierte Copy- oder Move-Assignment-Operator von DataArray aufgerufen?

    @wob sagte in Zuweisung an Referenz auf Basisklasse:

    Anstelle das irgendwie zu fixen: wäre es für dich nicht sinnvoller, wenn du lieber überall mit const& arbeiten würdest und den neuen Wert als Return-Value zurückgeben würdest anstelle von inplace-Änderungen?

    Die inplace-Änderung kommt daher, dass ich noch weitere Methoden dieser Art habe in anderen abgeleiteten Klassen habe und bei manchen bleibt der Typ und die Dimension von DataArray bestehen, sodass es effizienter ist die Werte einfach zu überschreiben. Mit const& müsste ich Speicher für einen neuen DataArray allokieren, was es insgesamt ineffizienter macht.

    Danke für die Hilfe. Ich denke ich muss tatsächlich die Pointer-Variante nehmen.

    Viele Grüße



  • @Max3000 sagte in Zuweisung an Referenz auf Basisklasse:

    @wob sagte in Zuweisung an Referenz auf Basisklasse:

    Warum nimmt diese Funktion denn ein DataArray entgegen, wenn die aber in Wirklichkeit eine Matrix als Parameter benötigt?

    Es gibt neben FlatteningLayer noch weitere Klassen, die die gleiche Methode implementieren, die aber einen Vector als Input benötigen. Darum der Cast am Anfang.

    Naja, aber dann müssten die ja auch alle casten. Können die nicht einfach einen anderen Parameter nehmen? Du kannst die Funktionen dann ja sowieso nicht austauschen bzw. auf beliebige DataArray-Argumente anwenden. Explizite dynamic_casts würde ich versuchen zu vermeiden.

    OK, das sehe ich ein. Dann wird der automatisch generierte Copy- oder Move-Assignment-Operator von DataArray aufgerufen?

    Der hat doch so eine Signatur wie DataArray& DataArray::operator=(const DataArray& rhs) - d.h. rechts vom = muss ein DataArray (oder ein von DA erbendes Objekt) stehen. Eigentlich gar nicht verwunderlich, dass das geht.

    Die inplace-Änderung kommt daher, dass ich noch weitere Methoden dieser Art habe in anderen abgeleiteten Klassen habe und bei manchen bleibt der Typ und die Dimension von DataArray bestehen, sodass es effizienter ist die Werte einfach zu überschreiben. Mit const& müsste ich Speicher für einen neuen DataArray allokieren, was es insgesamt ineffizienter macht.

    So in dieser Allgemeinheit ist das häufig anders als man denkt. Außerdem arbeitet es sich mit nicht-ändernden Operationen im Allgemeinen viel einfacher. Vielleicht musst du nur einfach sauber die inplace-Operationen von den anderen trennen. Dein dynamic_cast oben kann auch Performace kosten. (man muss i.d.R. für solche Aussagen benchmarken)


  • Gesperrt

    @Max3000 sagte in Zuweisung an Referenz auf Basisklasse:

    Pointer wollte ich hier aber eigentlich vermeiden. Geht es irgendwie mit Referenzen?

    Das sollte funktionieren, wenn du den Zuweisungsoperator überlädst... Ein Beispiel dafür findet sich hier:

    https://stackoverflow.com/questions/728986/does-assignment-operator-work-with-different-types-of-objects

    Beachte aber, dass dann verkettete Zuweisungen (a = b = c;) ggf. nicht mehr wie Soll funktionieren...


    Und dann noch: Eine Matrix ist kein Vector (formal ja, konkret aber nein)... ein einfacher Type-Cast wird also nicht funktionieren.

    Btw: Unter einem "DataArray" stelle ich mir eigentlich nur einen Vector vor, keine ganze Matrix...


  • Gesperrt

    Ich hab hier mal ein Beispiel, wie es funktionieren könnte... ist aber nicht besonders schön:

    #include <iostream>
    #include <algorithm>
    
    class Parent
    {
    protected:
        Parent(int a) : lessImportantVar(a) {}
        ~Parent() = default;
    public:
        int lessImportantVar;
        virtual void doSomething() = 0;
    };
    
    class ChildA;
    
    class ChildB;
    
    class ChildA : public Parent
    {
        public:
            int veryImportantVar1;
            int veryImportantVar2;
            ChildA(int a, int b, int c);
            ChildA(const ChildB & a);
            virtual void doSomething();
            ChildB & operator=(ChildA & other);
    };
    
    class ChildB : public Parent
    {
        public:
            int veryImportantVar3;
            ChildB(int a, int b);
            ChildB(const ChildA & a);
            virtual void doSomething();
    };
    
    inline ChildA::ChildA(int a, int b, int c) : Parent(a) {
                veryImportantVar1 = b;
                veryImportantVar2 = c;
            }
    inline ChildA::ChildA(const ChildB & a) : Parent(a.lessImportantVar) {
                veryImportantVar1 = a.veryImportantVar3;
                veryImportantVar2 = 42;
            }
    inline void ChildA::doSomething() {
                std::cout   << lessImportantVar << " "
                            << veryImportantVar1 << " "
                            << veryImportantVar2 << "\n";
            }
    inline ChildB & ChildA::operator=(ChildA & other) {
                return *(new ChildB(other));
            }
    
    inline ChildB::ChildB(int a, int b) : Parent(a) {
                veryImportantVar3 = b;
            }
    inline ChildB::ChildB(const ChildA & a) : Parent(a.lessImportantVar) {
                veryImportantVar3 = a.veryImportantVar1;
            }
    inline void ChildB::doSomething() {
                std::cout   << lessImportantVar << " "
                            << veryImportantVar3 << "\n";
            }
    
    int main()
    {
        Parent * p1 = new ChildA(5, 6, 7);
        Parent * p2 = new ChildB(2, 1);
        ChildA * c1 = dynamic_cast<ChildA *>(p1);
        ChildB c2 = *c1;
        p1->doSomething();
        p2->doSomething();
        c1->doSomething();
        c2.doSomething();
        return 0;
    }
    

    Hier kann man also in Zeile 71 (ohne dynamic_cast) von ChildA zu ChildB konvertieren...


  • Gesperrt

    Mir sind noch Unstimmigkeiten aufgefallen...

    • Der Dtor sollte public sein
    • Die abstrakte Funktion doSomething() sollte const sein
    • Copy ctor und Operatorüberladung sollte eine const Referenz entgegennehmen
    • delete sollte natürlich aufgerufen werden, damit es keine Lecks gibt

    Hoffe, ich hab jetzt nicht noch etwas übersehen. 😉

    #include <iostream>
    #include <algorithm>
    
    class Parent
    {
    protected:
        Parent(int a) : lessImportantVar(a) {}
    public:
        int lessImportantVar;
        ~Parent() = default;
        virtual void doSomething() const = 0;
    };
    
    class ChildA;
    
    class ChildB;
    
    class ChildA : public Parent
    {
        public:
            int veryImportantVar1;
            int veryImportantVar2;
            ChildA(int a, int b, int c);
            ChildA(const ChildB & a);
            virtual void doSomething() const;
            ChildB & operator=(const ChildA & other);
    };
    
    class ChildB : public Parent
    {
        public:
            int veryImportantVar3;
            ChildB(int a, int b);
            ChildB(const ChildA & a);
            virtual void doSomething() const;
    };
    
    inline ChildA::ChildA(int a, int b, int c) : Parent(a) {
                veryImportantVar1 = b;
                veryImportantVar2 = c;
            }
    inline ChildA::ChildA(const ChildB & a) : Parent(a.lessImportantVar) {
                veryImportantVar1 = a.veryImportantVar3;
                veryImportantVar2 = 42;
            }
    inline void ChildA::doSomething() const {
                std::cout   << lessImportantVar << " "
                            << veryImportantVar1 << " "
                            << veryImportantVar2 << "\n";
            }
    inline ChildB & ChildA::operator=(const ChildA & other) {
                return *(new ChildB(other));
            }
    
    inline ChildB::ChildB(int a, int b) : Parent(a) {
                veryImportantVar3 = b;
            }
    inline ChildB::ChildB(const ChildA & a) : Parent(a.lessImportantVar) {
                veryImportantVar3 = a.veryImportantVar1;
            }
    inline void ChildB::doSomething() const {
                std::cout   << lessImportantVar << " "
                            << veryImportantVar3 << "\n";
            }
    
    int main()
    {
        Parent * p1 = new ChildA(5, 6, 7);
        Parent * p2 = new ChildB(2, 1);
        const ChildA & c1 = dynamic_cast<ChildA&>(*p1);
        const ChildB & c2 = c1;
        p1->doSomething();
        p2->doSomething();
        c1.doSomething();
        c2.doSomething();
        delete p1;
        delete p2;
        return 0;
    }
    


  • Hallo allerseits.

    Danke erstmal für eure Antworten. Ich versuche mal meine Gedanken dazu zusammenzufassen.

    @wob sagte in Zuweisung an Referenz auf Basisklasse:

    So in dieser Allgemeinheit ist das häufig anders als man denkt. Außerdem arbeitet es sich mit nicht-ändernden Operationen im Allgemeinen viel einfacher. Vielleicht musst du nur einfach sauber die inplace-Operationen von den anderen trennen. Dein dynamic_cast oben kann auch Performace kosten. (man muss i.d.R. für solche Aussagen benchmarken)

    Da hast du Recht, der dynamic_cast gefällt mir auch absolut nicht, aber ich sehe nicht wie ich das hier vermeiden kann. Hier mal ein Beispiel wie mein Aufruf aussehen soll:

    // Define input  
    DataArray* x_tmp = input; // = Matrix oder Vector
      
    // Add layers
    std::vector<std::unique_ptr<Layer>> layers;
    layers.emplace_back(std::make_unique<MatrixInputLayer>(10, 10));     // In: Matrix, out: Matrix
    layers.emplace_back(std::make_unique<FlatteningLayer>(10, 10));      // In: Matrix, out: Vector
    layers.emplace_back(std::make_unique<FullyConnectedLayer>(10, 100)); // In: Vector, out: Vector
    layers.emplace_back(std::make_unique<FullyConnectedLayer>(1, 10));   // In: Vector, out: Vector
    
    // Evaluate data
    for(auto layer_it = layers.begin(); layer_it != layers.end(); ++layer_it)    
      (*layer_it)->forward_propagate(x_tmp);
    
    double res = (*x_tmp)[0];
    

    Die forward_propagate-Methode ist in allen von Layer abgeleiteten Klassen implementiert. Problem ist, dass es manchmal zwischen Matrix und Vektor wechselt. Hier ein Paar Beispiele:

    // Vektor rein, Vektor raus
    void FullyConnectedLayer::forward_propagate(DataArray*& x_) const
    {
      Vector* x = dynamic_cast<Vector*>(x_);
      *x = activate(weight * (*x) + bias, act);
    }
    // Matrix rein, Vektor raus
    void FlatteningLayer::forward_propagate(DataArray*& x_) const
    {  
      Matrix* x = dynamic_cast<Matrix*>(x_);
      x_ = new Vector(x->flatten());
      delete x;
    }
    

    Meine Basis-Klasse sieht übrigens so aus:

    class Layer
    {
     public:
      virtual void forward_propagate(DataArray*&) const;
      // ...
    };
    

    und forward_propagate ist in allen abgeleiteten Klassen override. Ich habe jetzt schon mehrere Varianten ausprobiert, aber an irgendeiner Stelle ist der Code immer unschön. Wenn man das ohne Vererbung macht muss man stattdessen an x Stellen den genauen Typ des Layers abfragen, was auch nicht so schön ist.


    @nameName sagte in Zuweisung an Referenz auf Basisklasse:

    Und dann noch: Eine Matrix ist kein Vector (formal ja, konkret aber nein)... ein einfacher Type-Cast wird also nicht funktionieren.
    Btw: Unter einem "DataArray" stelle ich mir eigentlich nur einen Vector vor, keine ganze Matrix...

    Ja eigentlich schon. Beide speichern Daten in einem Array. Die Einträge einer Matrix werden einfach nur Zeilenweise in das Array geschrieben. Der Unterschied in der Datenstruktur ist lediglich, dass man sich bei der Matrix noch die Anzahl der Zeilen speichern muss. Bei mir sieht das ungefähr so aus:

    class DataArray
    {
    // ...
    protected:
      double* data;
      size_t size;
    };
    
    class Vector : public DataArray
    {
      // ...
    };
    class Matrix : public DataArray
    {
      // ...
    private:
      size_t n_rows;
    };
    

    @nameName sagte in Zuweisung an Referenz auf Basisklasse:

    Ich hab hier mal ein Beispiel, wie es funktionieren könnte... ist aber nicht besonders schön:

    Danke für deine Mühen. Die Idee ist gut, an sowas habe ich auch gedacht, mich aber dann dagegen entschieden. Das ist meiner Meinung nach nicht das, was Vererbung tun sollte, Copy-Assignment und Copy-Construktor für ChildA auf ChildB und anders rum zu implementieren. Im Idealfall sollten ja die abgeleiteten Klassen nicht mal was voneinander wissen. Wobei mich dein Code auf eine Idee gebracht hat. Statt mit einem dynamic_cast könnte mein Code auch mit sowas beginnen:

    Matrix x = x_->reshape(dim1, dim2);
    

    Die zu erwartende Größe der Matrizen und Vektoren habe ich mir sowieso im Layer abgespeichert.

    Nachteil ist dennoch, dass ich immer ein neues Objekt erzeuge, also evtl. viel Speicher allokiere anstatt den vorhandenen DataArray zu überschreiben.

    Ich danke euch allen!
    Viele Grüße



  • Wäre es dann nicht besser, du würdest einen Vector einfach als 1xN (bzw. Nx1) Matrix darstellen? Dann brauchst du keine Vererbungshierarchie und unnötige Umrechnungsfunktionen.



  • @Th69 sagte in Zuweisung an Referenz auf Basisklasse:

    Wäre es dann nicht besser, du würdest einen Vector einfach als 1xN (bzw. Nx1) Matrix darstellen? Dann brauchst du keine Vererbungshierarchie und unnötige Umrechnungsfunktionen.

    Quasi im Matlab-Style, alles ist eine Matrix. Theoretisch ginge das, allerdings macht es den Code an anderer Stelle etwas unleserlich. In den Klassen Vektor und Matrix sind noch weitere Rechenoperationen definiert, die eben nur für diesen Typ funktionieren, teilweise auch unterschiedlich sind. Beispielsweise möchte ich auf einzelne Elemente zugreifen können. Bei Vektoren will ich

    vec[i]
    

    schreiben können, und bei Matrizen

    mat[i][j]
    

    Letzteres wird so realisiert, dass der erste operator[] eine Proxy-Klasse MatrixRow zurückgibt und für den gibt operator[] wieder eine Zahl zurück. Wenn alles eine Matrix ist müsste ich dann immer schreiben

    vec[1][i]
    

    Solche Geschichten machen den Code dann an vielen Stellen etwas unschöner. Ferner möchte ich mir die Option offen halten das ganze noch auf Tensoren, also Arrays mit n Indizes, zu erweitern.

    Danke und viele Grüße



  • Du könntest die einzelnen "Layer" auch zusammenstoppeln indem du jedem Layer den darüberliegenden Layer als Quelle mitgibst. Dann kannst du unterschiedliche abstrakte Klassen für Vektor-Quellen, Matrix-Quellen und Tensor-Quellen machen.

    Quasi so in der Art:

    class VectorSource {
    public:
        virtual ~VectorSource() = default;
        virtual Vector const& getData() = 0;
    };
    
    class MatrixSource {
    public:
        virtual ~MatrixSource() = default;
        virtual Matrix const& getData() = 0;
    };
    
    class MatrixInputLayer : public MatrixSource  {
        explicit MatrixInputLayer(Matrix m) : m_matrix{m} {}
        Matrix const& getData() override {
            return m_matrix;
        }
    
    private:
        Matrix m_matrix;
    };
    
    class FlatteningLayer : public VectorSource {
        explicit FlatteningLayer(MatrixSource* source) : m_source{source} {}
        Vector const& getData() override {
            Matrix const& m = m_source->getData();
            // flatten m into m_output
            return m_output;
        }
    
        MatrixSource* const m_source;
        Vector m_output;
    };
    


  • Oder aber du macht es einfach so:

    struct Data {
        Vector v;
        Matrix m;
        Tensor t;
    };
    

    Und dann mit der forward_propagate so wie du sie schon hast.

    Ein Vorteil davon wäre dass du in den Klassen wo sich der Typ ändert nicht mehr dynamisch Speicher anfordern musst. Wobei das eigentlich nur von Bedeutung ist wenn der ganze Prozess mehrfach wiederholt wird.


Anmelden zum Antworten