Verständnis / Anforderungen für Polymorphismus
Um den Polymorphismus zu verstehen - wie der Begriff in der Informatik verwendet wird -, ist es hilfreich, von einem einfachen Test und seiner Definition auszugehen. Erwägen:
Type1 x;
Type2 y;
f(x);
f(y);
Hier f()
soll eine Operation ausgeführt werden und es werden Werte x
und y
als Eingaben gegeben.
Um Polymorphismus zu zeigen, f()
muss in der Lage sein, mit Werten von mindestens zwei unterschiedlichen Typen (z. B. int
und double
) zu arbeiten und unterschiedlichen typgerechten Code zu finden und auszuführen.
C ++ - Mechanismen für Polymorphismus
Expliziter vom Programmierer spezifizierter Polymorphismus
Sie können so schreiben f()
, dass es auf verschiedene Arten mit mehreren Typen arbeiten kann:
Vorverarbeitung:
#define f(X) ((X) += 2)
// (note: in real code, use a longer uppercase name for a macro!)
Überlastung:
void f(int& x) { x += 2; }
void f(double& x) { x += 2; }
Vorlagen:
template <typename T>
void f(T& x) { x += 2; }
Virtueller Versand:
struct Base { virtual Base& operator+=(int) = 0; };
struct X : Base
{
X(int n) : n_(n) { }
X& operator+=(int n) { n_ += n; return *this; }
int n_;
};
struct Y : Base
{
Y(double n) : n_(n) { }
Y& operator+=(int n) { n_ += n; return *this; }
double n_;
};
void f(Base& x) { x += 2; } // run-time polymorphic dispatch
Andere verwandte Mechanismen
Vom Compiler bereitgestellter Polymorphismus für eingebaute Typen, Standardkonvertierungen und Gießen / Zwang werden der Vollständigkeit halber später erläutert als:
- Sie werden sowieso ohnehin intuitiv verstanden (was eine " oh, das " -Reaktion rechtfertigt ).
- Sie wirken sich auf die Schwelle für die Anforderung und die nahtlose Verwendung der oben genannten Mechanismen aus
- Erklärung ist eine fummelige Ablenkung von wichtigeren Konzepten.
Terminologie
Weitere Kategorisierung
Angesichts der oben genannten polymorphen Mechanismen können wir sie auf verschiedene Arten kategorisieren:
1 - Vorlagen sind äußerst flexibel. SFINAE (siehe auch std::enable_if
) erlaubt effektiv mehrere Sätze von Erwartungen für parametrischen Polymorphismus. Sie können beispielsweise codieren, dass Sie, wenn der von Ihnen verarbeitete Datentyp ein .size()
Mitglied hat, eine Funktion verwenden, andernfalls eine andere Funktion, die nicht benötigt wird .size()
(aber vermutlich in irgendeiner Weise leidet - z. B. strlen()
wenn Sie die langsamere verwenden oder nicht drucken als nützlich eine Nachricht im Protokoll). Sie können auch Ad-hoc-Verhaltensweisen angeben, wenn die Vorlage mit bestimmten Parametern instanziiert wird, wobei einige Parameter entweder parametrisch bleiben ( teilweise Vorlagenspezialisierung ) oder nicht ( vollständige Spezialisierung ).
"Polymorph"
Alf Steinbach kommentiert, dass sich Polymorphic im C ++ Standard nur auf Laufzeitpolymorphismus mit virtuellem Versand bezieht. General Comp. Sci. Die Bedeutung ist gemäß dem Glossar des C ++ - Erstellers Bjarne Stroustrup ( http://www.stroustrup.com/glossary.html ) umfassender:
Polymorphismus - Bereitstellung einer einzigen Schnittstelle zu Entitäten verschiedener Typen. Virtuelle Funktionen bieten dynamischen (Laufzeit-) Polymorphismus über eine Schnittstelle, die von einer Basisklasse bereitgestellt wird. Überladene Funktionen und Vorlagen bieten statischen Polymorphismus (zur Kompilierungszeit). TC ++ PL 12.2.6, 13.6.1, D & E 2.9.
Diese Antwort bezieht - wie die Frage - C ++ - Funktionen auf die Comp. Sci. Terminologie.
Diskussion
Mit dem C ++ Standard unter Verwendung einer engeren Definition von "Polymorphismus" als mit dem Comp. Sci. Gemeinschaft, um gegenseitiges Verständnis für Ihr Publikum zu gewährleisten, berücksichtigen Sie ...
- unter Verwendung einer eindeutigen Terminologie ("Können wir diesen Code für andere Typen wiederverwendbar machen?" oder "Können wir den virtuellen Versand verwenden?" anstelle von "Können wir diesen Code polymorph machen?") und / oder
- Definieren Sie Ihre Terminologie klar.
Entscheidend für einen großartigen C ++ - Programmierer ist jedoch, zu verstehen, was Polymorphismus wirklich für Sie bedeutet ...
Sie können einmal "algorithmischen" Code schreiben und ihn dann auf viele Datentypen anwenden
... und dann seien Sie sich sehr bewusst, wie unterschiedliche polymorphe Mechanismen Ihren tatsächlichen Bedürfnissen entsprechen.
Laufzeitpolymorphismus passt:
- Eingabe, die mit Factory-Methoden verarbeitet und als heterogene Objektsammlung ausgespuckt wurde, die über
Base*
s,
- Implementierung zur Laufzeit basierend auf Konfigurationsdateien, Befehlszeilenoptionen, UI-Einstellungen usw. ausgewählt,
- Die Implementierung variierte zur Laufzeit, z. B. für ein Zustandsmaschinenmuster.
Wenn es keinen eindeutigen Treiber für den Laufzeitpolymorphismus gibt, sind Optionen zur Kompilierungszeit häufig vorzuziehen. Erwägen:
- Der so genannte Kompilierungsaspekt von Vorlagenklassen ist Fat-Interfaces vorzuziehen, die zur Laufzeit ausfallen
- SFINAE
- CRTP
- Optimierungen (viele davon einschließlich Inlining und Eliminierung von totem Code, Abrollen von Schleifen, statische stapelbasierte Arrays gegen Heap)
__FILE__
, __LINE__
, Stringliteral Verkettung und andere Fähigkeiten von Makros (die bleiben böse ;-))
- Die semantische Verwendung von Vorlagen und Makros für Tests wird unterstützt, schränkt jedoch die Bereitstellung dieser Unterstützung nicht künstlich ein (da der virtuelle Versand dazu neigt, genau übereinstimmende Überschreibungen der Elementfunktionen zu erfordern).
Andere Mechanismen, die den Polymorphismus unterstützen
Wie versprochen werden der Vollständigkeit halber mehrere periphere Themen behandelt:
- Vom Compiler bereitgestellte Überladungen
- Umbauten
- Abgüsse / Zwang
Diese Antwort schließt mit einer Diskussion darüber, wie das oben Genannte kombiniert wird, um polymorphen Code zu stärken und zu vereinfachen - insbesondere parametrischen Polymorphismus (Vorlagen und Makros).
Mechanismen zur Zuordnung zu typspezifischen Operationen
> Implizite vom Compiler bereitgestellte Überladungen
Konzeptionell überlastet der Compiler viele Operatoren für integrierte Typen. Es unterscheidet sich konzeptionell nicht von benutzerdefinierter Überladung, wird jedoch aufgelistet, da es leicht übersehen wird. Beispielsweise können Sie int
s und double
s mit derselben Notation hinzufügen, x += 2
und der Compiler erzeugt:
- typspezifische CPU-Anweisungen
- ein Ergebnis des gleichen Typs.
Das Überladen erstreckt sich dann nahtlos auf benutzerdefinierte Typen:
std::string x;
int y = 0;
x += 'c';
y += 'c';
Vom Compiler bereitgestellte Überladungen für Basistypen sind in Hochsprachen (3GL +) häufig anzutreffen, und die explizite Diskussion des Polymorphismus impliziert im Allgemeinen etwas mehr. (Bei 2GLs - Assemblersprachen - muss der Programmierer häufig explizit unterschiedliche Mnemoniken für unterschiedliche Typen verwenden.)
> Standardkonvertierungen
Der vierte Abschnitt des C ++ - Standards beschreibt Standardkonvertierungen.
Der erste Punkt fasst gut zusammen (aus einem alten Entwurf - hoffentlich immer noch im Wesentlichen korrekt):
-1- Standardkonvertierungen sind implizite Konvertierungen, die für integrierte Typen definiert sind. Klausel conv listet den gesamten Satz solcher Konvertierungen auf. Eine Standardkonvertierungssequenz ist eine Sequenz von Standardkonvertierungen in der folgenden Reihenfolge:
Keine oder eine Konvertierung aus dem folgenden Satz: Konvertierung von Wert zu Wert, Konvertierung von Array zu Zeiger und Konvertierung von Funktion zu Zeiger.
Null oder eine Konvertierung aus dem folgenden Satz: Integral-Promotions, Gleitkomma-Promotion, Integral-Konvertierungen, Gleitkomma-Konvertierungen, Gleitkomma-Integral-Konvertierungen, Zeiger-Konvertierungen, Zeiger-zu-Mitglied-Konvertierungen und Boolesche Konvertierungen.
Keine oder eine Qualifikationsumwandlung.
[Hinweis: Eine Standardkonvertierungssequenz kann leer sein, dh sie kann aus keinen Konvertierungen bestehen. ] Eine Standardkonvertierungssequenz wird bei Bedarf auf einen Ausdruck angewendet, um ihn in einen erforderlichen Zieltyp zu konvertieren.
Diese Konvertierungen ermöglichen Code wie:
double a(double x) { return x + 2; }
a(3.14);
a(42);
Anwenden des früheren Tests:
Um polymorph zu sein, a()
muss [ ] in der Lage sein, mit Werten von mindestens zwei unterschiedlichen Typen (z. B. int
und double
) zu arbeiten und typgerechten Code zu finden und auszuführen .
a()
selbst führt Code speziell für aus double
und ist daher nicht polymorph.
Beim zweiten Aufruf a()
des Compilers muss jedoch ein typgerechter Code für eine "Gleitkomma-Heraufstufung" (Standard §4) generiert werden , in die konvertiert werden 42
soll 42.0
. Dieser zusätzliche Code befindet sich in der aufrufenden Funktion. Wir werden die Bedeutung davon in der Schlussfolgerung diskutieren.
> Zwang, Casts, implizite Konstruktoren
Mit diesen Mechanismen können benutzerdefinierte Klassen Verhaltensweisen angeben, die den Standardkonvertierungen der integrierten Typen ähneln. Werfen wir einen Blick:
int a, b;
if (std::cin >> a >> b)
f(a, b);
Hier wird das Objekt std::cin
mit Hilfe eines Konvertierungsoperators in einem booleschen Kontext ausgewertet. Dies kann konzeptionell mit "Integral Promotions" et al. Aus den Standard-Conversions im obigen Thema gruppiert werden.
Implizite Konstruktoren machen effektiv dasselbe, werden jedoch vom Cast-to-Typ gesteuert:
f(const std::string& x);
f("hello"); // invokes `std::string::string(const char*)`
Auswirkungen von vom Compiler bereitgestellten Überladungen, Konvertierungen und Zwang
Erwägen:
void f()
{
typedef int Amount;
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
Wenn wir möchten, dass der Betrag x
während der Division als reelle Zahl behandelt wird (dh 6,5 anstatt auf 6 abgerundet), müssen wir nur auf ändern typedef double Amount
.
Das ist schön, aber es wäre nicht zu viel Arbeit gewesen, den Code explizit "richtig eingeben" zu lassen:
void f() void f()
{ {
typedef int Amount; typedef double Amount;
Amount x = 13; Amount x = 13.0;
x /= 2; x /= 2.0;
std::cout << double(x) * 1.1; std::cout << x * 1.1;
} }
Bedenken Sie jedoch, dass wir die erste Version in eine template
:
template <typename Amount>
void f()
{
Amount x = 13;
x /= 2;
std::cout << x * 1.1;
}
Aufgrund dieser kleinen "Komfortfunktionen" kann es so einfach instanziiert werden, dass es entweder funktioniert int
oder double
wie beabsichtigt funktioniert. Ohne diese Funktionen benötigen wir explizite Casts, Typmerkmale und / oder Richtlinienklassen, einige ausführliche, fehleranfällige Fehler wie:
template <typename Amount, typename Policy>
void f()
{
Amount x = Policy::thirteen;
x /= static_cast<Amount>(2);
std::cout << traits<Amount>::to_double(x) * 1.1;
}
Vom Compiler bereitgestellte Operatorüberladung für integrierte Typen, Standardkonvertierungen, Casting / Zwang / implizite Konstruktoren - alle tragen subtil zur Unterstützung des Polymorphismus bei. Aus der Definition oben in dieser Antwort geht hervor, dass "typgerechten Code gefunden und ausgeführt wird", indem Folgendes zugeordnet wird:
Sie erstellen keine polymorphen Kontexte selbst, sondern helfen dabei, Code in solchen Kontexten zu stärken / zu vereinfachen.
Sie fühlen sich vielleicht betrogen ... es scheint nicht viel zu sein. Die Bedeutung besteht darin, dass wir in parametrischen polymorphen Kontexten (dh innerhalb von Vorlagen oder Makros) versuchen, einen beliebig großen Bereich von Typen zu unterstützen, aber häufig Operationen an ihnen in Form anderer Funktionen, Literale und Operationen ausdrücken möchten, die für a entwickelt wurden kleiner Satz von Typen. Es reduziert die Notwendigkeit, nahezu identische Funktionen oder Daten pro Typ zu erstellen, wenn die Operation / der Wert logisch identisch ist. Diese Funktionen wirken zusammen, um eine Haltung der "besten Anstrengung" hinzuzufügen, indem sie das tun, was intuitiv erwartet wird, indem sie die begrenzten verfügbaren Funktionen und Daten verwenden und nur dann mit einem Fehler aufhören, wenn echte Unklarheiten bestehen.
Dies hilft dabei, den Bedarf an polymorphem Code zu begrenzen, der polymorphen Code unterstützt, ein engeres Netz um die Verwendung von Polymorphismus zu ziehen, damit die lokalisierte Verwendung keine weit verbreitete Verwendung erzwingt, und die Vorteile des Polymorphismus nach Bedarf verfügbar zu machen, ohne die Kosten für die Offenlegung bei zu verursachen Kompilierungszeit, mehrere Kopien derselben logischen Funktion im Objektcode zur Unterstützung der verwendeten Typen und beim virtuellen Versand im Gegensatz zu Inlining- oder zumindest zur Kompilierungszeit aufgelösten Aufrufen. Wie in C ++ üblich, hat der Programmierer viel Freiheit, die Grenzen zu steuern, innerhalb derer Polymorphismus verwendet wird.