2014-08-28 10 views
5

Ich untersuche derzeit das Zusammenspiel zwischen polymorphen Typen und Zuweisungsoperationen. Mein Hauptanliegen ist, ob jemand versuchen könnte, den Wert einer Basisklasse einem Objekt einer abgeleiteten Klasse zuzuweisen, was zu Problemen führen würde.Erkennung der Zuordnung der Basisklasse zum Referenzieren auf die abgeleitete Klasse

Von this answer habe ich gelernt, dass der Zuweisungsoperator der Basisklasse immer durch den implizit definierten Zuweisungsoperator der abgeleiteten Klasse verborgen ist. Für die Zuweisung zu einer einfachen Variablen führen falsche Typen zu Compilerfehlern. Allerdings ist dies nicht der Fall, wenn die Zuordnung über eine Referenz auftritt:

class A { public: int a; }; 
class B : public A { public: int b; }; 
int main() { 
    A a; a.a = 1; 
    B b; b.a = 2; b.b = 3; 
    // b = a; // good: won't compile 
    A& c = b; 
    c = a; // bad: inconcistent assignment 
    return b.a*10 + b.b; // returns 13 
} 

Diese Form der Zuordnung wahrscheinlich Objektzustand führen würde inconcistent, jedoch gibt es keine Compiler-Warnung und der Code sieht nicht böse auf mich zuerst Blick.

Gibt es ein etabliertes Idiom, um solche Probleme zu erkennen?

Ich denke, ich kann nur auf die Laufzeit-Erkennung hoffen, werfen eine Ausnahme, wenn ich eine solche ungültige Zuordnung finden. Der beste Ansatz, den ich mir gerade vorstellen kann, ist ein benutzerdefinierter Zuweisungsoperator in der Basisklasse, der Laufzeittypinformationen verwendet, um sicherzustellen, dass this tatsächlich ein Zeiger auf eine Instanz der Basis und nicht auf eine abgeleitete Klasse ist Führt eine manuelle Kopie von Mitglied zu Mitglied durch. Dies klingt nach viel Aufwand und beeinträchtigt die Lesbarkeit des Codes erheblich. Gibt es etwas leichter?

Bearbeiten: Da die Anwendbarkeit einiger Ansätze davon abhängt, was ich tun möchte, hier sind einige Details. Ich habe zwei mathematische Konzepte, sagen ring und field. Jedes Feld ist ein Ring, aber nicht umgekehrt. Es gibt mehrere Implementierungen für jede, und sie teilen gemeinsame Basisklassen, nämlich AbstractRing und , wobei letztere von der ersteren abgeleitet sind. Jetzt versuche ich eine leicht zu schreibende by-reference Semantik basierend auf std::shared_ptr zu implementieren. So enthält meine Klasse Ring eine std::shared_ptr<AbstractRing>, die ihre Implementierung enthält, und eine Reihe von Methoden, die an diese weiterleiten. Ich möchte Field als Erben von Ring schreiben, also muss ich diese Methoden nicht wiederholen. Die Methoden, die für ein Feld spezifisch sind, würden den Zeiger einfach auf AbstractField werfen, und ich würde das gerne statisch machen. Ich kann sicherstellen, dass der Zeiger tatsächlich ein AbstractField bei der Konstruktion ist, aber ich mache mir Sorgen, dass jemand eine Ring zu einer Ring& zuweisen wird, die eigentlich eine Field ist, womit meine angenommene Invariante über den enthaltenen geteilten Zeiger gebrochen wird.

+0

Ist hier nicht das eigentliche Problem, dass Sie eine nicht abstrakte Basisklasse haben? –

+1

Persönlich deaktiviere ich Kopierkonstruktor und Zuweisungsoperatoren für polymorphe Typen. Vererbungsbasis-Polymorphismus spielt wirklich nicht gut mit Werttypen. –

+0

@OliCharlesworth: Ich sehe nicht wie. Denken Sie z.B. Ein Toggle-Button, der von einer Schaltfläche abgeleitet ist, zeigt Situationen, in denen die Basisklasse instanziierbar sein sollte und das Problem in der realen Welt auftreten könnte. Daher würde ich keinem Ansatz "Alle Grundlagen müssen abstrakt sein" folgen. Wenn das nicht Ihr Ziel ist, dann erläutern Sie bitte, wie abstrakte Basisklassen in meiner Situation helfen können. – MvG

Antwort

1

Da die Zuordnung zu einem Downcast-Typ-Referenz zur Kompilierzeit nicht erkannt werden kann, würde ich eine dynamische Lösung vorschlagen. Es ist ein ungewöhnlicher Fall, und ich würde normalerweise dagegen sein, aber möglicherweise ist die Verwendung eines virtuellen Zuweisungsoperators erforderlich.

class Ring { 
    virtual Ring& operator = (const Ring& ring) { 
     /* Do ring assignment stuff. */ 
     return *this; 
    } 
}; 

class Field { 
    virtual Ring& operator = (const Ring& ring) { 
     /* Trying to assign a Ring to a Field. */ 
     throw someTypeError(); 
    } 

    virtual Field& operator = (const Field& field) { 
     /* Allow assignment of complete fields. */ 
     return *this; 
    } 
}; 

Dies ist wahrscheinlich der sinnvollste Ansatz.

Eine Alternative könnte darin bestehen, eine Vorlagenklasse für Referenzen zu erstellen, die dies verfolgen kann und die Verwendung von einfachen Zeigern * und Referenzen & einfach verbietet. Eine Vorlagenlösung ist möglicherweise schwieriger zu implementieren, würde aber eine statische Typprüfung ermöglichen, die den Downcast verbietet. Hier ist eine Basisversion, die zumindest für mich korrekt einen Kompilierungsfehler gibt, wobei "noDerivs (b)" der Ursprung des Fehlers ist, wobei GCC 4.8 und das Flag -std = C++ 11 (für static_assert) verwendet werden.

#include <type_traits> 

template<class T> 
struct CompleteRef { 
    T& ref; 

    template<class S> 
    CompleteRef(S& ref) : ref(ref) { 
     static_assert(std::is_same<T,S>::value, "Downcasting not allowed"); 
     } 

    T& get() const { return ref; } 
    }; 

class A { int a; }; 
class B : public A { int b; }; 

void noDerivs(CompleteRef<A> a_ref) { 
    A& a = a_ref.get(); 
} 

int main() { 
    A a; 
    B b; 
    noDerivs(a); 
    noDerivs(b); 
    return 0; 
} 

Diese spezifische Vorlage noch täuschen kann, wenn der Benutzer zum ersten Mal eine Referenz seines eigenen erzeugt und übergibt das als Argument. Am Ende ist es ein hoffnungsloses Unterfangen, deine Benutzer davor zu bewahren, dumme Dinge zu tun. Manchmal ist alles, was Sie tun können, eine faire Warnung zu geben und eine detaillierte Best-Practice-Dokumentation zu präsentieren.

+0

Interessant. "Zuordnung zu einem Downcast-Typ-Verweis kann zur Laufzeit nicht erkannt werden": In C++ 11 können Sie 'typeid (* this) == typeid (Ring)' innerhalb der 'Ring :: operator = (...)' Implementierung ausführen , was ich mir vorgestellt habe. Nicht sicher, ob der virtuelle Operator eine bessere oder schlechtere Leistung hat, müsste das eines Tages testen. "Dies ist der Standard-Zuweisungsmodus in Java." Aber Java hat Referenzsemantik, daher sehe ich nicht, auf welche Art von Aufgabe Sie hier Bezug nehmen. Die Zuordnung von Array-Mitgliedern kommt mir nahe, sieht aber immer noch anders aus. Dieser Java-Aspekt ist wahrscheinlich ein Thema, aber ich bin neugierig. – MvG

+0

@MvG Der Typeid-Vergleich wird zur Laufzeit mit RTTI durchgeführt. Die virtuelle Zuweisung verwendet die virtuelle Klassen-Tabelle und erfordert keinen Vergleich. Im Allgemeinen werden virtuelle Member in C++ bevorzugt, aber ich bin mir nicht sicher, ob RTTI im Vergleich dazu einen signifikanten Overhead liefert. Der virtuelle Anruf ist eine "Einzeiger-Indirection", während RTTI mehr (nicht ganz sicher) beinhalten kann. Mein Java ist ein wenig eingerostet. Ich glaube, ich meine eher das Objekt .equals und .clone als die Zuweisung, aber es gibt ähnliche Semantiken. – Zoomulator

+0

Ich habe gerade festgestellt, dass ich Laufzeit geschrieben habe, als ich kompilierte Zeit meinte! – Zoomulator

Verwandte Themen