Polimorphismus in Parameterübergabe (2D Array)



  • -.- ups hab übersehen, dass es java ist, hab nur mit nem halben auge drüber geguckt.

    mh, das ist aber ne gute frage wie man mehrdimensionale arrays mit polymorphie übergibt

    ich denk mal was nach



  • #include <iostream>
    
    class Base 
    {
    public:
    	int getInt() const { return 0x10; }
    };
    
    class Derive
    	: public Base
    {
    public:
    	float getFloat() const { return 0.5f; }
    };
    
    void doSomething(Base const * const x, size_t count)
    {
    	while ( count-- )
    		std::cout << (x+count)->getInt() << std::endl;
    }
    
    int main()
    {
    	Derive d[10];
    
    	doSomething(d, 10);
    
    	system("PAUSE");
    	return 0;
    }
    

    also sowas geht schonmal fehlerfrei, was dir klar sein sollte um das nächste zu verstehen.

    #include <iostream>
    
    class Base 
    {
    public:
    	int getInt() const { return 0x10; }
    };
    
    class Derive
    	: public Base
    {
    public:
    	float getFloat() const { return 0.5f; }
    };
    
    void doSomething(Base ** arr, int x, int y)
    {
    	for(int i = 0; i < x; ++i) {
    		for(int j = 0; j < y; ++j) {
    			std::cout << arr[i][j].getInt() << ", ";
    		}
    		std::cout << std::endl;
    	}
    }
    
    int main()
    {
    	const int x = 10;
    	const int y = 5;
    	Derive d[x][y];
    
    	doSomething(reinterpret_cast<Base**>(&d), x, y);
    
    	system("PAUSE");
    	return 0;
    }
    

    das wäre jetzt die variante, dass es so funktioniert wie du es gerne hättest. aber shcön ist das nicht. überhaupt ist die frage warum du mit 2D arrays hantierst, da wird es sicher bessere alternativen geben. und nur weil es in java geht, heisst es nicht dass es gut ist. java ist viel abstrakter und hardware ferner als C/C++

    edit: und wenn du noch böser sein willst, ersetz das hier

    Derive d[x][y];
    doSomething(reinterpret_cast<Base**>(&d), x, y);
    

    durch

    std::vector<std::vector<Derive>> d(x, std::vector<Derive>(y));
    doSomething(reinterpret_cast<Base**>(&d), x, y);
    

  • Mod

    "Never treat arrays polymorphically" (und das gilt im Prinzip auch für jede andere Form von Container, die nur einen einzelnen Typ als Containerelement enthalten).

    Wäre ein Container<B> wie ein Container<A> benutzbar (B sei von A abgeleitet), würde nichts dich hindern, ein Objekt des Typs A, dass kein B ist, in diesen Container einzufügen - damit würde offensichtlich eine Invariante des Container<B> verletzt werden.
    Es kann mitunter sinnvoll sein, einen polymorphen Observer-Container zu unterstützen, das ist aber jedenfalls für die Standardcontainer nicht vorgesehen. Der Aufwand dürfte den Nutzen gerade für einen Anfänger in C++ auch kaum rechtfertigen.

    @Skym0sh0: das ist UB.



  • #include <iostream>
    #include <vector>
    #include <memory>
    
    class Base 
    {
    public:
    	int getInt() const { return 0x10; }
    };
    
    class Derive
    	: public Base
    {
    public:
    	float getFloat() const { return 0.5f; }
    };
    
    void doSomething(std::vector<std::vector<std::shared_ptr<Base>>> arr)
    {
    	for(int i = 0; i < arr.size(); ++i) {
    		for(int j = 0; j < arr[i].size(); ++j) {
    			std::cout << arr[i][j]->getInt() << ", ";
    		}
    		std::cout << std::endl;
    	}
    }
    
    int main()
    {
    	const int x = 10;
    	const int y = 5;
    
    	std::vector<std::vector<std::shared_ptr<Derive>>> d(x, std::vector<std::shared_ptr<Derive>>(y));
    
    	doSomething(d);
    
    	system("PAUSE");
    	return 0;
    }
    

    sowas wäre maximal noch denkbar, ist aber nicht kompilationsfähig...
    aus oben genannten gründen



  • Fabi1986 schrieb:

    In Java ist das problemlos möglich

    Java verbirgt einiges unter der Haube, das dir hier das Genick bricht. Zudem sehe ich hier auch keine Polymorphie, die sich nach meinen Verständnis u.a. mit virtuellen Methoden beschäftigt.

    Mehrere Dinge:
    1. In C++ verwendet man new in der Regel sparsam. Mit hoher Wahrscheinlichkeit jedenfalls nicht zum erzeugen einer lokalen Variable.
    1.1 C++ kennt keinen Garbage Collector, zu jedem new gehört ein delete.

    int main() // <-- void stellt keine Nutzinformation dar, lasse ich daher weg-
    {
      D d; // Objekt kann auf dem Stack angelegt werden, ist bequemer wenn man
           // keine Zeiger braucht.
      ...
      d.setX(c);
      ...
      // exit(0); <-- Setzt man eigentlich niemals ein
    }
    

    2. Man schreibt üblicherweise in C++ nicht für freie Funktionen Klassen.

    // Ohne Klasse:
    void setX(
      vector<vector<B> > & b) // <-- >> kann missinterpretiert werden (Mit C++11 behoben)
    {                         // <-- Objekte versus Referenzen (dazu unten mehr)
      b[0][0].x=2;
    } // <-- Hinter eine Funktionsdefinition gehört kein Semikolon
    

    3. Du musst in C++ zwischen 3 wesentlichen Sachen unterscheiden:
    a) Objekt (Objekt liegt auf dem Stack).
    b) Referenz (Aliasname für ein Objekt, Alias muss immer auf ein gültiges Objekt verweisen, und kann nicht auf ein anderes Objekt umgebogen werden).
    c) Zeiger (Objekt liegt auf dem Heap, im Zeiger steht die Adresse, unter der es im Heap gefunden wird)

    Beginnen wir mal ganz simpel:

    Ein Objekt liegt direkt auf dem Stack, und dies auch ganz. Falls wir folgende Situation haben...

    class A
    {
      public:
        int x;
    };
    
    class B : public A
    {
      public:
        int y;
    };
    
    int main()
    {
      A a1;
      a1.x = 1;
      A a2;
      a1.x = 2;
      B b1;
      b1.x = 3;
      b1.y = 4;
      B b2;
      b2.x = 5;
      b2.y = 6;
      // <-- Ich beschreibe den Punkt hier
    }
    

    Sähe das auf dem Stack in etwa wie folgt aus:

    0x00  1 - Objekt a1
    0x04  2 - Objekt a2
    0x08  3 \ Objekt b1  <-- A-Anteil
    0x10  4 /            <-- B-Anteil
    0x14  5 \ Objekt b2
    0x18  6 /
    

    Würdest du nun eine Zuweisung von b1 an b2 vornehmen, käme dies zu einem "Slicing" da ein Element A nur einen int annehmen kann. Du würdes den B-Anteil abschneiden.

    Ändern wir das Beispiel in Zeiger ab, sähe es etwas anders aus:

    ...
    int main()
    {
      A * a1 = new A;
      a1->x = 1;
      // ... (<-- Obigen Code entsprechend auf Zeiger umgestellt.
      delete a1; // Zu jedem new ein delete
      // ...
    }
    

    Nun lägen die eigentlichen Objekte auf dem Heap, und a1,a2,b1,b2 enthielten nur die Adresse, quasi die "Hausnummer" wo das Objekt beginnt. Die Zeiger selbst lägen auf dem Stack (stark vereinfacht):

    Stack:
    0x0000  1000 - Zeiger a1 (Enthält Adresse auf Objekt)
    0x0004  1004 - Zeiger a2 (Enthält Adresse auf Objekt)
    0x0008  1008 - Zeiger b1 (Enthält Adresse auf Objekt)
    0x0010  1018 - Zeiger b2 (Enthält Adresse auf Objekt)
    
    Heap:
    0x1000  1 - Objekt a1
    0x1004  2 - Objekt a2
    0x1008  3 \ Objekt b1  <-- A-Anteil
    0x1010  4 /            <-- B-Anteil
    0x1014  5 \ Objekt b2
    0x1018  6 /
    

    Eine Zuweisung von b1 auf a1 würde nun nur die Anfangsadresse setzen, es träte kein Slicing auf (a1 würde dann wie b1 die Adresse 1008 enthalten). Wobei falls du nicht vorher a1 freigibst eine "Leiche" auf dem Heap verbleiben würde: Ein allokierter Speicherbereich, der nicht freigegeben wurde.

    Eine Referenz wiederum kann nur auf ein gültiges Objekt verweisen, und ist nur ein anderer Name für diese Variable:

    ...
    int main()
    {
      // A & test; <-- Fehler, es muss auf ein bestehendes Objekt verwiesen werden
      A a1;
      a1.x = 1;
      A & a2 = a1; // a2 ist ein Alias, ein alternativer Name für a1.
      a2.x = 2; // Ändert a1.x!
    }
    
    0x00  1 - Objekt a1 (und Referenz a2, da nur anderer Name für a1)
    

    Javareferenzen entsprechen Syntaktisch C++ Objekten/Referenzen, intern aber eher den C++ Zeigern (Nur Zeiger können auf 0/NULL verweisen, nur Zeiger kann man zur Laufzeit auf andere Objekte verweisen lassen).

    Gehen wir nun zu Parametern weiter:

    Im wesentlichen kann man Funktionsparameter als Wert (Objekt), Referenz oder Zeiger übergeben.

    void foo1(
      int a) // <-- Übergabe wird Kopiert, eigenes Objekt
    {
      a = 2; // Nur die Kopie wird geändert
    }
    
    void foo2(
      int & a) // <-- Übergabe des Ursprungsobjektes unter anderem Namen
    {
      a = 3; // Ursprungsobjekt wird geändert
    }
    
    void foo3(
      int * a) // <-- Übergabe einer Adresse (die Kopiert wird)
    {
      // Achtung: NULL/0 ist für a zulässig!
      *a = 4; // Änderung über die Adresse, damit wird das Ursprungsobjekt geändert.
    }
    
    int main()
    {
      int i = 1;
      foo1(i); // i anschließend noch immer 1
      foo2(i); // i anschließend 2
      foo3(&i); // i anschließend 3
      // foo3(0); bzw. foo3(NULL); // Zugriffsverletzung
    }
    

    So... nun zum nächsten Punkt.

    4. vector<A> und vector<B> sind unterschiedliche Typen. Selbst wenn B von A erbt. Selbst wenn eine Zuweisung möglich wäre, die vectoren enthalten Objekte. Es würde zu Slicing (siehe oben) kommen.

    5. Laufzeit-Polymorphie funktioniert logischerweise nur mit Zeigern oder Referenzen!

    // Klassen wie im obigen Beispiel
    
    void foo1(
      A a);
    void foo2(
      B b);
    void foo3(
      A & a);
    void foo4(
      A * a);
    
    int main()
    {
      B b;
      foo1(b); // Wenn es funktioniert würde nur der A-Anteil kopiert werden (Slicing)
      foo2(b); // Okay, B wird kopiert
      foo3(b); // Dies ist kein Problem und möglich
      foo3(&b); // Dies ist kein Problem und möglich
    }
    

    Nur ein paar Dinge die erstmal einwirken müssen...


  • Mod

    #include <iostream>
    #include <vector>
    #include <memory>
    
    class Base
    {
    public:
        int getInt() const { return 0x10; }
    };
    
    class Derive
        : public Base
    {
    public:
        float getFloat() const { return 0.5f; }
    };
    
    template <typename T>
    class vector_vector_transformer : public std::vector<std::vector<T>>
    {
    public:
        template <typename U>
        vector_vector_transformer(const std::vector<std::vector<U>>& v)
        {
            for ( auto it = v.begin(); it != v.end(); ++it )
                this->push_back( std::vector<T>( it->begin(), it->end() ) );
        }
    };
    
    void doSomething(vector_vector_transformer<std::shared_ptr<Base>> arr)
    {
        for(int i = 0; i < arr.size(); ++i) {
            for(int j = 0; j < arr[i].size(); ++j) {
                std::cout << arr[i][j]->getInt() << ", ";
            }
            std::cout << std::endl;
        }
    }
    
    int main()
    {
        const int x = 10;
        const int y = 5;
    
        std::vector<std::vector<std::shared_ptr<Derive>>> d(x, std::vector<std::shared_ptr<Derive>>(y));
    
        doSomething(d);
    
        system("PAUSE");
        return 0;
    }
    

    ist allerdings nicht genau das, was gefragt war.



  • @Skym0sh0
    Danke, ich werde mir deine Beispiele mal angucken. Ich brauche das Feld allerdings im Heap 🙂 (kann groß werden) ist das dann trotzdem noch so möglich? Aber wenn ja, bekomm ich denke das sicher auch alleine hin 🙂

    Also vielen Dank!!!

    @camper
    Danke für deine Belehrung.
    Ich bin (oh wunder 🙂 ) kein erfahrener Software-Architekt 🙂 .
    Meine Idee bei dem ganzen ist folgende:
    "Base" ist in meinem Programm ein Kontainer für Daten.
    "Derived" stellt Methoden zur verfügung die auf den Daten arbeiten.
    main löst ein Problem anhand von Methoden aus "Derived" mit den Daten aus "Base"
    eine Hilfsklasse "doSomething" soll die Daten in "Base" befüllen.
    sowohl "doSomething" als auch die mainKlasse + "Derived" sollen austauchbar sein, "Base" der gemeinsame nenner.

    naja, ich schaue mal inwiefern ich das umstricken kann.

    Auf jedenfall danke an euch
    Gruß Fabi



  • Fabi1986 schrieb:

    Meine Idee bei dem ganzen ist folgende:
    "Base" ist in meinem Programm ein Kontainer für Daten.
    "Derived" stellt Methoden zur verfügung die auf den Daten arbeiten...

    Für mich hört sich das eher wie sehr schlechtes Design an. Kannst du genauer beschreiben was du eigentlich willst? Was willst du konkret damit erreichen?



  • Für mich hört sich das eher wie sehr schlechtes Design an. Kannst du genauer beschreiben was du eigentlich willst? Was willst du konkret damit erreichen?

    Das kann gut sein 🙂 c++ hab ich grad neu gelernt, ich kannte bisher nur Java und wenn ich die Probleme geahnt hätte hät ich das vieleicht anders gemacht (werd ich jetzt wohl auch umstricken müssen.)

    Ich versuche meine Idee mal zu beschreiben:
    Ich habe ein Lösungs-Programm was ein physikaliches Problem berechnet. Wie die Daten in das Programm kommen soll austauchbar sein (über Parameter, über Gui etc.), das heist die Klasse die die Daten einfüllt (in meiner Analogie die Hilfsklasse D) wird als Parameter an mein Programm übergeben. Die Implementation dieser Klasse soll also von dem Problemlöser unabhängig bleiben. Desweiteren (und hier muss ich warscheinlich umstricken, weil ich es mir da zu einfach machen wollte) soll auch die art der Lösung also mein Lösungsprogramm austauchbar sein. Der gemeinsame Nenner war hier die Basisklasse denn die Daten sind immer gleich, egal wie sie ins Programm kommen und wie sie gelöst werden. die Abgeleitete Klasse stellt Methoden auf die Daten zur verfügung die aber abhängig von dem Lösungsprogramm sind, dh. die Hilfsklasse darf diese Implementation nicht kennen um auch für andere Lösungsklassen verwendbar zu sein.

    Ich vermute von diesem Gedanken muss ich mich verabschieden, also für jede Lösungsklassenimplementation eine eigenen Hilfsklasse erstellen.



  • Das hat aber jetzt gar nichts mit C++ zu tun, das ist OOP, und damit in Java genau so. Dein Lösungsalgo wird auf den Daten arbeiten, dazu muss (und sollte) man nicht ableiten. Die Daten kommen von außen, das heißt sie müssen nicht mal Teil (Member) der Klasse sein, sondern ein Parameter der Einstiegsfunktion des Algo würde reichen. Wenn jetzt auch der Algo austauschbar sein soll, schaffst du dir eine Basiklasse (In Java wäre wohl ein reines Interface) die eine entsprechende Schnittstelle anbietet, welche von den einzelnen konkreten Algo-Inkarnationen implementiert werden müssen. Die Basisklasse ist dabei NICHT die Datenklasse, sondern von dieser unabhängig!



  • Fabi1986 schrieb:

    c++ hab ich grad neu gelernt, ich kannte bisher nur Java und wenn ich die Probleme geahnt hätte hät ich das vieleicht anders gemacht (werd ich jetzt wohl auch umstricken müssen.)

    Die Unterschiede zwischen Java und C++ sind vielschichtiger als man meint. Auch designt man in C++ und Java in der Regel unterschiedlich (z.B. zieht man üblicherweise nicht alles in Klassen). Wenn du länger mit C++ zu tun haben solltest, würde ich dir raten einen Blick in "Scott Meyers: Effective C++ Programming" zu werfen.

    Ein Fehler der aber unabhängig von der Sprache ist, ist die falsche Verwendung der Vererbung. Ich behaupte (zumindest nach dem was ich in realen Projekten bemerkt habe) das mindestens 60-80% aller Vererbungen die ich bislang gesehen habe unnötig, wenn nicht sogar gravierende Designfehler dargestellt haben.

    Merk dir allgemein das eine lose Bindung in der Regel immer besser ist als eine starke Bindung, und Vererbung stellt eine der stärksten Bindungen überhaupt dar (in C++ von friend getoppt). Vererbungshierarchien sind meistens schwerer zu erweitern oder zu ändern als beispielsweise Kompositionen. Öffentliche Vererbung sollte nur verwendet werden wenn man wirklich sagen kann "Ist ein", und die Klassen auch wirklich für Vererbung ausgelegt wurden sind.


Anmelden zum Antworten