polsmorphe klasse mit operator=()



  • Weil ein Basisobjekt ein Basisobjekt ist ?

    Also ein Fahrzeug ein Fahrzeug und kein Schiff Panzer oder sonst was ?!

    Ich kann ein Basisobjekt mit einem Basisobjekt vergleichen wie ein int mit einem int. Ich kann aber kein std::string mit einem int vergleichen wie ich kein schiff mit einem panzer vergleichen kann.



  • Vielleicht würden sich alle leichter tun wenn du statt Autos und Booten mal konkret schreibst um was es geht (oder hab ich das überlesen?).
    Ansonsten sehe ich das Problem nicht. Die Lösung heisst typeid. Guckst du:

    #include <cassert>
    #include <string>
    #include <memory>
    #include <iostream>
    
    class Vehicle
    {
    public:
    	Vehicle(int maxSpeed)
    		:	m_maxSpeed(maxSpeed)
    	{
    	}
    
    	bool operator == (Vehicle const& other) const // final
    	{
    		// virtual call to CompareImpl of whatever class (*this) is
    		return CompareImpl(other);
    	}
    
    	bool operator != (Vehicle const& other) const // final
    	{
    		// virtual call to CompareImpl of whatever class (*this) is
    		return !CompareImpl(other);
    	}
    
    	// virtual interface:
    	virtual std::string Foo()
    	{
    		return "Vehicle";
    	}
    
    protected:
    	// compare the Vehicle part of 2 Vehicles
    	bool CompareVehicles(Vehicle const& other) const
    	{
    		return m_maxSpeed == other.m_maxSpeed;
    	}
    
    private:
    	// only ever called by operators == and != of Vehicle
    	// override in every derived class
    	virtual bool CompareImpl(Vehicle const& other) const
    	{
    		assert(typeid(*this) == typeid(Vehicle)); // (*this) MUST be a Vehicle (and NO derived class)
    		if(typeid(other) == typeid(Vehicle))
    			return CompareVehicles(other);
    		else
    			return false;
    	}
    
    	int m_maxSpeed;
    };
    
    class Car
    	:	public Vehicle
    {
    public:
    	Car(int maxSpeed, int wheelCount)
    		:	Vehicle(maxSpeed),
    			m_wheelCount(wheelCount)
    	{
    	}
    
    	// virtual interface:
    	virtual std::string Foo()
    	{
    		return "Car";
    	}
    
    protected:
    	// compare the Car part of 2 Cars
    	bool CompareCars(Car const& other) const
    	{
    		return CompareVehicles(other) && (m_wheelCount == other.m_wheelCount);
    	}
    
    private:
    	// see Vehicle::CompareImpl
    	virtual bool CompareImpl(Vehicle const& other) const
    	{
    		assert(typeid(*this) == typeid(Car)); // (*this) MUST be a Car (and NO derived class)
    		if(typeid(other) == typeid(Car))
    			// since we now know the exact type of (other) we don't need a slow dynamic_cast
    			return CompareCars(static_cast<Car const&>(other));
    		else
    			return false;
    	}
    
    	int m_wheelCount;
    };
    
    class Boat
    	:	public Vehicle
    {
    public:
    	Boat(int maxSpeed, int draught)
    		:	Vehicle(maxSpeed),
    			m_draught(draught)
    	{
    	}
    
    	// virtual interface:
    	virtual std::string Foo()
    	{
    		return "Boat";
    	}
    
    protected:
    	bool compareBoats(Boat const& other) const
    	{
    		return CompareVehicles(other) && (m_draught == other.m_draught);
    	}
    
    private:
    	// see Vehicle::CompareImpl
    	virtual bool CompareImpl(Vehicle const& other) const
    	{
    		assert(typeid(*this) == typeid(Boat)); // (*this) MUST be a Boat (and NO derived class)
    		if(typeid(other) == typeid(Boat))
    			// since we now know the exact type of (other) we don't need a slow dynamic_cast
    			return compareBoats(static_cast<Boat const&>(other));
    		else
    			return false;
    	}
    
    	int m_draught;
    };
    
    int main(int argc, char** argv)
    {
    	std::auto_ptr<Vehicle> vehicle(new Vehicle(10));
    	std::auto_ptr<Vehicle> car1(new Car(10, 4));
    	std::auto_ptr<Vehicle> car2(new Car(10, 4));
    	std::auto_ptr<Vehicle> car3(new Car(10, 6));
    	std::auto_ptr<Vehicle> boat1(new Boat(10, 4));
    	std::auto_ptr<Vehicle> boat2(new Boat(10, 4));
    	std::auto_ptr<Vehicle> boat3(new Boat(10, 99));
    
    	assert(*vehicle == *vehicle);
    	assert(*car1 != *vehicle);
    	assert(*boat1 != *vehicle);
    	assert(*car1 == *car2);
    	assert(*car1 != *car3);
    	assert(*car2 != *car3);
    	assert(*boat1 == *boat2);
    	assert(*boat1 != *boat3);
    	assert(*boat2 != *boat3);
    
    	assert(*car1 != *boat1);
    	assert(*car2 != *boat1);
    	//...
    
    	std::cout << "Vehicle: " << vehicle->Foo() << std::endl;
    	std::cout << "Car: " << car1->Foo() << std::endl;
    	std::cout << "Boat: " << boat1->Foo() << std::endl;
    	std::cout << std::flush;
    
    	return 0;
    }
    

    Funktioniert wunderbar. Und typeid(a) == typeid(b) würde ich immer einem dynamic_cast vorziehen, ganz einfach weil es schneller ist, und weil du mit dynamic_cast einen doofen Tanz aufführen musst um draufzukommen ob denn das zu testende Objekt wirklich ein "Bar" ist oder nur ein von "Bar" abgeleitetes etwas. Gehen tut es schon, sieht dann aber so aus (*würg*):

    Vehicle* v = GimmeSomeVehicle();
    	// see if v is a car (and NOT an instance of a derived type)
    	if((dynamic_cast<Car*>(v) != 0) && (dynamic_cast<void*>(v) == static_cast<Car*>(v)))
    		DoSomethingWithCar(static_cast<Car*>(v));
    

    Erklärung: dynamic_cast<void*> gibt die Adresse des "most derived type" zurück. Wenn diese gleich der Adresse von "v auf Car gecastet" ist, dann muss v ein Car sein und kein von Car abgeleiteter Typ. Da wir aber nicht einfach v auf Car* casten können ohne überhaupt zu wissen ob v ein Car oder von Car abgeleiteter Typ ist müssen wir das vorher nochmal prüfen, daher der Test (dynamic_cast<Car*>(v) != 0).

    Wie gesagt alles viel zu kompliziert, von daher lieber typeid verwenden.

    Falls die Objekte um die es geht serialisierung unterstützen ist es überhaupt recht einfach, denn dann musst du bloss die serialisierten Daten vergleichen. In den serialisierten Daten muss ja wohl eine Art eindeutiger Typenkennung vorkommen, sonst kannst du sie ja nie mehr deserialisieren.

    Ahja, das Zuweisen... . Entweder du klonst wirklich die Objekte anstatt Zuweisungn zu verwenden (dann musst du eben damit leben das Alte wegzuwerfen und ein Neues zu machen), oder aber du wirst auf das Problem stossen dass du die Zuweisung "DerivedType = BaseType;" irgendwie behandeln musst. Entweder man verbietet es und du wirfst einen std::logic_error, oder aber du musst den DerivedType mit irgendwelchen Defaultwerten füttern. Auch doof. Lässt sich aber auf jeden Fall gleich wie die Operatoren == und != oben umsetzen.

    Im übrigen gehe ich auch davon aus dass es ein eleganteres Design geben müsste. Kann ich aber nicht sagen solange du nicht sagst worum es konkret geht.



  • Hi,

    nur mal als andere Technik reingeworfen (habe keine Ahnung, ob das für Dich passt): operator==() kann man auch als freie binäre Funktion definieren. Es muß keine Memberfunktion sein.
    Damit könntest Du entsprechende Overloads für jedes Typpaar bereitstellen (vielleicht soll ja ein Vergleich "LKW == Schiff" anders vonstatten gehen als "LKW == Auto" ...)
    Kann natürlich recht aufwendig sein, wenn man viele Typen hat ... und man sollte die "symmetrische Version" nicht vergessen, damit "LKW == Schiff" nichts anderes ergibt als "Schiff == LKW" ... es sei denn man will genau das.
    Und man muß sich natürlich "Generiziztät" ein wenig anders nähern.

    Nur mal so als Idee....
    (gilt natürlich auch für operator=())

    Gruß,

    Simon2.



  • Simon2 schrieb:

    nur mal als andere Technik reingeworfen (habe keine Ahnung, ob das für Dich passt): operator==() kann man auch als freie binäre Funktion definieren. Es muß keine Memberfunktion sein.
    Damit könntest Du entsprechende Overloads für jedes Typpaar bereitstellen (vielleicht soll ja ein Vergleich "LKW == Schiff" anders vonstatten gehen als "LKW == Auto" ...)

    Ne, geht nicht. C++ kann keine multimethods.

    Du hantierst ja nur mit Base* rum... und dann nimmt C++ auch nur den Typ Base* zum aufloesen welche Funktion aufgerufen wird.



  • Shade Of Mine schrieb:

    ...Du hantierst ja nur mit Base* rum...

    Stimmt schon, ist ja Laufzeitpolymorphie - hatte ich im ersten Elan gar nicht gesehen. *hand-an-die-stirn-klatsch*
    Dabei hilft natürlich die (template-)STL und das Overloading mit ihrer Compilezeitpolymorphie nicht wirklich weiter ....

    Tja, wenn's Laufzeit sein muß, kann ich auch nicht weiterhelfen.

    Schönen Abend noch,

    Simon2.



  • Shade Of Mine schrieb:

    Wenn du es wirklich willst, und wie gesagt, ich halte es fuer eher kaputt, kannst du natuerlich die standard variante mit dynamic_cast machen (was anderes geht nicht):

    virtual void compare(Base& o) {
      Derived& other=dynamic_cast<Derived&>(other);
      return this.foo == other.foo;
    }
    

    Hehe.. dachte schon dass sich da ein kleiner std::bad_cast eingeschlichen hat. Aber es geht ja gar nicht um equals geht.

    Meine Variante:

    virtual int compare( base const& other ) const
    {
      throw std::runtime_exception("This method shouldn't exist, let alont be called!");
    }
    
    virtual bool equals( base const& other ) const
    {
      // if you must, why the heck not?
      if (derived const* d = dynamic_cast<derived const*>(&other)) {
        return some_attribute() == other->some_attribute();
      } else {
        return false;
      }
    }
    


  • finix schrieb:

    Hehe.. dachte schon dass sich da ein kleiner std::bad_cast eingeschlichen hat.

    der ist absicht.

    damits "bumm" geht, wenn jemand mist baut.
    aber das ist halt je nachdem wie man es lieber hat...



  • Ist mir dann auch klar geworden. Dachte ja auch erst es geht um equals.



  • Knuddlbaer schrieb:

    Weil ein Basisobjekt ein Basisobjekt ist ?

    Also ein Fahrzeug ein Fahrzeug und kein Schiff Panzer oder sonst was ?!

    Ich kann ein Basisobjekt mit einem Basisobjekt vergleichen wie ein int mit einem int. Ich kann aber kein std::string mit einem int vergleichen wie ich kein schiff mit einem panzer vergleichen kann.

    So? Ich kann ein Schiff mit einem Panzer vergleichen. Und solange ich zu dem Ergebnis komme, daß sie ungleich sind, will ich mal denjenigen sehen, der mir da widerspricht
    🙂



  • Plotzenhotz schrieb:

    Vielleicht würden sich alle leichter tun wenn du statt Autos und Booten mal konkret schreibst um was es geht (oder hab ich das überlesen?).

    Nein, hast du nicht überlesen. Da müsste ich Euch die halbe Anwendung erklären. Glaubt mir einfach daß Boote und Fahrzeuge es genau so tun.

    Plotzenhotz schrieb:

    Ansonsten sehe ich das Problem nicht. Die Lösung heisst typeid. Guckst du:

    (...)

    Sehr gute Lösung, das habe ich gesucht. Meine bisherige Lösung ähnelt deiner ein wenig, aber deine hat einen großen Vorteile: Bei mir wäre für jede Vererbungsebene einmal eine RTTI-Operation gemacht worden (ich habe keine weitere, interne Vegleichsmethode benutzt). Darum wäre bei mir der dynamic_cast zwar einfacher gewesen, als in deinem Beispiel, aber eben verschachtelt. Deine Lösung ist viel besser.

    Nebenbei gesagt, kannte ich die variante dynamic_cast<void*>() noch nicht. Asche auf mein Haupt.

    Was das typeid anbelangt, frage ich mich, warum hast du den Vergleich nicht generell in operator ==() gemacht? Also so:

    bool Vehicle::operator == (Vehicle const& other) const
        {
            if(typeid(other) == typeid(*this))
                return CompareImpl(other);
            else
                return false;
        }
    
    // .......
    
        virtual bool Car::CompareImpl(Vehicle const& other) const
        {
            assert(typeid(*this) == typeid(Car));
            assert(typeid(other) == typeid(Car));
            return CompareCars(static_cast<Car const&>(other));
        }
    

    Das spart 'ne Menge Code, nicht? Habe ich was übersehen?

    Plotzenhotz schrieb:

    Funktioniert wunderbar. Und typeid(a) == typeid(b) würde ich immer einem dynamic_cast vorziehen, ganz einfach weil es schneller ist.

    Es ist schneller? Hast du den Nerv mir das zu erklären?

    Plotzenhotz schrieb:

    Falls die Objekte um die es geht serialisierung unterstützen ist es überhaupt recht einfach, denn dann musst du bloss die serialisierten Daten vergleichen. In den serialisierten Daten muss ja wohl eine Art eindeutiger Typenkennung vorkommen, sonst kannst du sie ja nie mehr deserialisieren.

    Daran hatte ich gedacht, aber die Objekte, in meinem Fall, sind (z.Zt.) nicht serialisierbar. Auch wäre diese Lösung wohl ziemlich langsam.

    Plotzenhotz schrieb:

    Ahja, das Zuweisen... . Entweder du klonst wirklich die Objekte anstatt Zuweisungn zu verwenden (dann musst du eben damit leben das Alte wegzuwerfen und ein Neues zu machen), oder aber du wirst auf das Problem stossen dass du die Zuweisung "DerivedType = BaseType;" irgendwie behandeln musst. Entweder man verbietet es und du wirfst einen std::logic_error, oder aber du musst den DerivedType mit irgendwelchen Defaultwerten füttern. Auch doof. Lässt sich aber auf jeden Fall gleich wie die Operatoren == und != oben umsetzen.

    Defaultwerte ist in der Tat dämlich. Da ich im Augenblick keine wirkliche Notwendigkeit habe, Elemente über Basiszeiger polymorph *zuzuweisen*, habe ich mich entschieden zu Clonen. Damit erledigen sich solche Zicken.
    Ich habe für meinen polymorphen Container nur einen operator=() geschrieben der alle Elemente des Containers löscht und Clone der Elemente des zugewiesenen Containers einhängt.
    Ich habe keine Lust, ohne echte Not "DerivedType = BaseType;" zu behandeln. Im Gegensatz zu "Auto == Schiff" muss ich nämlich den Vorredner recht geben, daß sich "Auto = Schiff" fürchterlich beißt.

    Du hast mir wirklich Klarheit geschaffen. Nicht nur deine == Lösung ist besser, ich bin jetzt auch sicher, daß es keine Lösung ohne RTTI gibt und daß ich nichts elementares übersehen habe. Vielen Dank!



  • Ja, ich war gedanklich viel zu wenig bei der RTTI geschichte.

    Ich stell mich ja schon ins Eck 🤡



  • Jordy schrieb:

    ...

    Plotzenhotz schrieb:

    Funktioniert wunderbar. Und typeid(a) == typeid(b) würde ich immer einem dynamic_cast vorziehen, ganz einfach weil es schneller ist.

    Es ist schneller? Hast du den Nerv mir das zu erklären?
    ...

    Ich bin zwar eigentlich nicht angesprochen, aber der Angesprochene schrieb hier:

    Plotzenhotz schrieb:

    ...Ich weiss bloss dass MSVC strcmp() aufruft - und je tiefer er suchen muss, umso öfter. Und strcmp() ist halt langsamer als Hashcodes oder gleich UIDs zu vergleichen.

    Ob das andere Compiler anders machen weiss ich nicht, kann leicht sein dass die meisten Compiler hersteller dynamic_cast nicht als wichtig genug sehen um das ordentlich zu optimieren.

    Der Vergleich von type_info Instanzen ist übrigens auch über strcmp() implementiert, aber da wird wenigstens immer bloss ein String verglichen.

    Gruß,

    Simon2.



  • Jordy schrieb:

    Plotzenhotz schrieb:

    Vielleicht würden sich alle leichter tun wenn du statt Autos und Booten mal konkret schreibst um was es geht (oder hab ich das überlesen?).

    Nein, hast du nicht überlesen. Da müsste ich Euch die halbe Anwendung erklären. Glaubt mir einfach daß Boote und Fahrzeuge es genau so tun.

    * Was ist das für eine Liste?
    * Für welche Operation(en) brauchst du den Test auf Gleichheit?
    * etc



  • Was das typeid anbelangt, frage ich mich, warum hast du den Vergleich nicht generell in operator ==() gemacht? Also so: (...)

    Weil ich nicht daran gedacht habe bzw. es mir nicht aufgefallen ist 🙂
    Das verwendete Pattern ist halt allgemein, und mir ist nicht aufgefallen dass ich ja nen Teil der Funktionalität in dem Fall in den operator == raufziehen kann. Normalerweise ist die public Funktion die die virtuelle private "Impl" Funktion aufruft ein dummer Stub der selber nix tut.

    Nochwas zur Frage warum type_info mit == zu vergleichen schneller ist als ein dynamic_cast: ich weiss nicht 100% ob das mit allen Compilern der Fall ist, aber ich denke bei type_info == vs. dynamic_cast *sollte* type_info == immer besser abschneiden. Einfach deswegen weil nur 2 konkrete Typen verglichen werden müssen, und nicht ein Typ in einer (möglicherweise recht langen) Liste von erreichbaren (und vom Ausgangspunkt aus eindeutig erreichbaren - vonwegen "multiple inheritance" ohne "virtual inheritance" und so - "the dreaded diamond") Basistypen gesucht werden muss. Selbst wenn man die Suche in der Liste irgendwie optimiert wird man es wohl kaum auf einen einzigen Vergleich/Lookup reduzieren können.

    Was ich aber nicht ganz verstehe ist warum MSVC nicht wenigstens pre-hashed Strings hernimmt, wenn er schon unbedingt nen String Vergleich machen muss. Das ginge dann doch etwas flotter. Zwar nur eine lineare Optimierung, dafür sicher > Faktor 2 (wahrscheinlich eher Faktor 10 oder mehr für lange Namen die sich erst weit hinten unterscheiden).

    Daran hatte ich gedacht, aber die Objekte, in meinem Fall, sind (z.Zt.) nicht serialisierbar. Auch wäre diese Lösung wohl ziemlich langsam.

    Naja, wie langsam das ist kommt darauf an wie man serialisiert und wohin. Im Normalfall wirst du aber zumindest eine dynamische Speicheranforderung haben, und das kostet auch nicht gerade wenig. Allerdings wahrscheinlich auch nicht um Welten langsamer als die type_info Lösung.

    ich bin jetzt auch sicher, daß es keine Lösung ohne RTTI gibt

    Jain. Es gibt keine Lösung wo du nicht die Klassen irgendwie eindeutig identifizieren musst, was aber nicht unbedingt über RTTI sein muss. Eine einfache "virtual std::string GetClassName() const" Funktion würde schon ausreichen, bloss is das wieder viel Aufwand das überall nachzuziehen, und auch z.T. gefählich, denn wenn jmd. vergisst z.B. SportsCar::GetClassName zu deklarieren wird Car::GetClassName hergenommen, dann gibts ganz hässliche Probleme die u.U. sehr blöd zu debuggen sind. Siehe auch MFC, die tun auch fest rumcasten und vergleichen ohne RTTI, halt über das selbstgestrickte CRuntimeClass System.


Anmelden zum Antworten