Polymorphismus in C ++

129

SO VIEL ICH WEISS:

C ++ bietet drei verschiedene Arten von Polymorphismus.

  • Virtuelle Funktionen
  • Überladung des Funktionsnamens
  • Überlastung des Bedieners

Zusätzlich zu den drei oben genannten Arten von Polymorphismus gibt es andere Arten von Polymorphismus:

  • Laufzeit
  • Kompilierungszeit
  • Ad-hoc-Polymorphismus
  • parametrischer Polymorphismus

Ich weiß, dass Laufzeitpolymorphismus durch virtuelle Funktionen und statischer Polymorphismus durch Vorlagenfunktionen erreicht werden kann

Aber für die anderen beiden

  • Ad-hoc-Polymorphismus
  • parametrischer Polymorphismus sagt die Website ,

Ad-hoc-Polymorphismus:

Wenn der Bereich der tatsächlichen Typen, die verwendet werden können, begrenzt ist und die Kombinationen vor der Verwendung individuell angegeben werden müssen, wird dies als Ad-hoc-Polymorphismus bezeichnet.

parametrischer Polymorphismus:

Wenn der gesamte Code ohne Erwähnung eines bestimmten Typs geschrieben wurde und somit transparent mit einer beliebigen Anzahl neuer Typen verwendet werden kann, spricht man von parametrischem Polymorphismus.

Ich kann sie kaum verstehen :(

Kann jemand sie beide wenn möglich anhand eines Beispiels erklären? Ich hoffe, die Antworten auf diese Fragen wären hilfreich für viele neue Passagen ihrer Colleges.

Vijay
quelle
30
Tatsächlich weist C ++ vier Arten von Polymorphismus auf: parametrisch (Generizität über Vorlagen in C ++), Inklusion (Subtypisierung über virtuelle Methoden in C ++), Überladung und Zwang (implizite Konvertierungen). Konzeptionell gibt es kaum einen Unterschied zwischen Funktionsüberladung und Bedienerüberladung.
Fredoverflow
Es scheint also, dass die Website, die ich erwähnt habe, viele irreführt. Bin ich richtig?
Vijay
@zombie: Diese Website berührt viele gute Konzepte, ist jedoch in ihrer Terminologie nicht präzise und konsistent (sobald sie beispielsweise über virtuellen Versand- / Laufzeitpolymorphismus spricht, macht sie viele Aussagen über Polymorphismus, die falsch sind im Allgemeinen aber wahr für den virtuellen Versand). Wenn Sie das Thema bereits verstehen, können Sie sich auf das beziehen, was gesagt wird, und die notwendigen Vorbehalte mental einfügen, aber es ist schwierig, durch Lesen der Website dorthin zu gelangen ...
Tony Delroy
Einige Begriffe sind nahezu synonym oder eher verwandt, aber eingeschränkter als andere Begriffe. Zum Beispiel wird der Begriff "Ad-hoc-Polymorphismus" meiner Erfahrung nach in Haskell hauptsächlich verwendet, aber "virtuelle Funktionen" sind sehr eng miteinander verbunden. Der kleine Unterschied besteht darin, dass "virtuelle Funktionen" ein objektorientierter Begriff ist, der sich auf Elementfunktionen mit "später Bindung" bezieht. "Mehrfachversand" ist auch eine Art Ad-hoc-Polymorphismus. Und wie FredOverflow sagt, sind sowohl die Überlastung von Bedienern als auch von Funktionen im Grunde dasselbe.
Steve314
Ich habe deine Formatierung für dich korrigiert. Bitte lesen Sie die Hilfe rechts im Bearbeitungsbereich. Jemand mit> 200 Fragen und> 3k sollte dieses grundlegende Zeug kennen. Vielleicht möchten Sie auch eine neue Tastatur kaufen. Die Umschalttaste dieses Benutzers scheint zeitweise zu versagen. Oh, und: In C ++ gibt es keine "Vorlagenfunktion" . Es gibt jedoch Funktionsvorlagen .
sbi

Antworten:

219

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 xund yals Eingaben gegeben.

Um Polymorphismus zu zeigen, f()muss in der Lage sein, mit Werten von mindestens zwei unterschiedlichen Typen (z. B. intund 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:

  • Wann wird der polymorphe typspezifische Code ausgewählt?

    • Laufzeit bedeutet, dass der Compiler Code für alle Typen generieren muss, die das Programm während der Ausführung verarbeiten kann, und zur Laufzeit der richtige Code ausgewählt wird ( virtueller Versand )
    • Kompilierungszeit bedeutet, dass die Auswahl des typspezifischen Codes während der Kompilierung erfolgt. Eine Konsequenz daraus: Sagen wir, ein Programm, das foben nur mit intArgumenten aufgerufen wurde - abhängig vom verwendeten polymorphen Mechanismus und den Inlining-Optionen kann der Compiler möglicherweise vermeiden, Code für zu f(double)generieren, oder der generierte Code wird möglicherweise irgendwann beim Kompilieren oder Verknüpfen weggeworfen. ( alle oben genannten Mechanismen außer virtuellem Versand )

  • Welche Typen werden unterstützt?

    • Ad-hoc bedeutet, dass Sie expliziten Code zur Unterstützung jedes Typs bereitstellen (z. B. Überladung, Vorlagenspezialisierung). Sie fügen ausdrücklich die Unterstützung "für diesen" Typ (gemäß der Ad-hoc -Bedeutung), einige andere "dies" und möglicherweise auch "das" hinzu ;-).
    • Parametrische Bedeutung Sie können einfach versuchen, die Funktion für verschiedene Parametertypen zu verwenden, ohne speziell etwas zu tun, um deren Unterstützung zu aktivieren (z. B. Vorlagen, Makros). Ein Objekt mit Funktionen / Operatoren, die sich wie die Vorlage / das Makro verhalten, erwartet 1, ist alles, was die Vorlage / das Makro für ihre Arbeit benötigt, wobei der genaue Typ irrelevant ist. Die von C ++ 20 eingeführten "Konzepte" drücken solche Erwartungen aus und setzen sie durch - siehe cppreference- Seite hier .

      • Parametrischer Polymorphismus ermöglicht das Tippen von Enten - ein Konzept, das James Whitcomb Riley zugeschrieben wird, der anscheinend sagte: "Wenn ich einen Vogel sehe, der wie eine Ente läuft und wie eine Ente schwimmt und wie eine Ente quakt, nenne ich diesen Vogel eine Ente." .

        template <typename Duck>
        void do_ducky_stuff(const Duck& x) { x.walk().swim().quack(); }
        
        do_ducky_stuff(Vilified_Cygnet());
        
    • Der Polymorphismus des Subtyps (auch bekannt als Inklusion) ermöglicht es Ihnen, an neuen Typen zu arbeiten, ohne den Algorithmus / die Funktion zu aktualisieren. Sie müssen jedoch von derselben Basisklasse abgeleitet sein (virtueller Versand).

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 ints und doubles mit derselben Notation hinzufügen, x += 2und 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. intund double) zu arbeiten und typgerechten Code zu finden und auszuführen .

a()selbst führt Code speziell für aus doubleund 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 42soll 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::cinmit 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 xwä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 intoder doublewie 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:

  • "weg" von Parametertypen

    • aus den vielen Datentypen polymorphe algorithmische Code-Handles

    • In dem Code für eine (möglicherweise geringere) Anzahl der (gleicher oder anderer) Arten geschrieben.

  • Parametertypen "bis" aus Werten des konstanten Typs

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.

Tony Delroy
quelle
1
-1 Gute Antwort bis auf die Terminologiediskussion. Der C ++ - Standard definiert den Begriff "polymorph" in §1.8 / 1 und bezieht sich dort auf Abschnitt 10.3 über virtuelle Funktionen. Es gibt also keinen Spielraum, keinen Raum für Diskussionen, keinen Raum für persönliche Meinungen: Im Kontext von Standard-C ++ wird dieser Begriff ein für alle Mal definiert. Und es spielt eine Rolle in der Praxis. Zum Beispiel dynamic_casterfordert §5.2.7 / 6 about einen "Zeiger auf oder einen Wert eines polymorphen Typs". Prost & hth.,
Prost und hth. - Alf
@Alf: tolle Referenz - obwohl ich denke, dass Ihre Perspektive zu eng ist. Aus der Frage, in der Überladung, Ad-hoc- und parametrischer Polymorphismus usw. aufgeführt sind, geht klar hervor, dass die Antwort die Fähigkeiten von C ++ mit dem allgemeinen Comp in Beziehung setzen sollte. Sci. Bedeutung der Begriffe. In Stroustrups Glossar heißt es in der Tat: "Polymorphismus - Bereitstellung einer einzigen Schnittstelle für Entitäten verschiedener Typen. Virtuelle Funktionen bieten dynamischen (Laufzeit-) Polymorphismus über eine von einer Basisklasse bereitgestellte Schnittstelle. Überladene Funktionen und Vorlagen bieten statischen (Kompilierungs-) Polymorphismus. TC ++ PL 12.2.6, 13.6.1, D & E 2.9. "
Tony Delroy
@ Tony: Es ist nicht der Hauptschub Ihrer Antwort ist falsch. Es ist in Ordnung, es ist großartig. es ist nur so wrt. Terminologie, die Sie verkehrt herum verstanden haben: Die formale akademische Terminologie ist die enge, die durch den Holy International Standard definiert ist, und die informelle grobe Terminologie, in der Menschen leicht unterschiedliche Bedeutungen haben können, wird hauptsächlich in dieser Frage und Antwort verwendet. Prost & hth.,
Prost und hth. - Alf
@Alf: Ich wünschte, die Antwort wäre großartig - "Andere Mechanismen" müssen in einem Fünftel der Zeilen neu geschrieben werden, und ich denke über einen konkreteren Kontrast zwischen den polymorphen Mechanismen nach und entwerfe ihn. Mein Verständnis ist jedenfalls, dass die formale akademische, ausschließlich C ++ - fokussierte Bedeutung eng sein mag, aber die formale akademische allgemeine Comp. Sci. Bedeutung ist nicht, wie aus Stroustrups Glossar hervorgeht. Wir brauchen etwas Bestimmtes - zB Definition von Knuth - noch kein Glück beim googeln. Ich schätze, dass Sie ein C ++ - Guru sind, aber können Sie diesbezüglich auf relevante Beweise hinweisen?
Tony Delroy
1
@Alf: Zweitens bin ich zuversichtlich, dass Polymorphismus in jedem anständigen allgemeinen Comp formal definiert ist. Sci. Buch auf eine (zeitlose, stabile) Weise, die mit meiner Nutzung (und der von Stroustrup) kompatibel ist. Der Wikipedia-Artikel verknüpft einige wissenschaftliche Veröffentlichungen, die dies so definieren: "Polymorphe Funktionen sind Funktionen, deren Operanden (tatsächliche Parameter) mehr als einen Typ haben können. Polymorphe Typen sind Typen, deren Operationen auf Werte von mehr als einem Typ anwendbar sind." (aus lucacardelli.name/Papers/OnUnderstanding.A4.pdf ). Die Frage ist also "Wer spricht für Comp. Sci" ...?
Tony Delroy
15

In C ++ ist die wichtige Unterscheidung die Laufzeit- und die Kompilierungsbindung. Ad-hoc vs. parametrisch hilft nicht wirklich, wie ich später erklären werde.

|----------------------+--------------|
| Form                 | Resolved at  |
|----------------------+--------------|
| function overloading | compile-time |
| operator overloading | compile-time |
| templates            | compile-time |
| virtual methods      | run-time     |
|----------------------+--------------|

Hinweis - Der Laufzeitpolymorphismus wird möglicherweise zur Kompilierungszeit noch aufgelöst, dies ist jedoch nur eine Optimierung. Die Notwendigkeit, die Laufzeitauflösung effizient zu unterstützen und gegen andere Probleme abzuwägen, ist Teil dessen, was dazu geführt hat, dass virtuelle Funktionen so sind, wie sie sind. Und das ist wirklich der Schlüssel für alle Formen des Polymorphismus in C ++ - jede ergibt sich aus verschiedenen Kompromissen, die in einem anderen Kontext gemacht wurden.

Funktionsüberladung und Bedienerüberladung sind in jeder Hinsicht dasselbe. Die Namen und die Syntax für ihre Verwendung haben keinen Einfluss auf den Polymorphismus.

Mit Vorlagen können Sie viele Funktionsüberladungen gleichzeitig angeben.

Es gibt eine andere Reihe von Namen für dieselbe Auflösungszeit-Idee ...

|---------------+--------------|
| early binding | compile-time |
| late binding  | run-time     |
|---------------+--------------|

Diese Namen sind eher mit OOP verbunden, daher ist es etwas seltsam zu sagen, dass eine Vorlage oder eine andere Nichtmitgliedsfunktion eine frühe Bindung verwendet.

Um die Beziehung zwischen virtuellen Funktionen und Funktionsüberladung besser zu verstehen, ist es auch nützlich, den Unterschied zwischen "Einzelversand" und "Mehrfachversand" zu verstehen. Die Idee kann als Fortschritt verstanden werden ...

  • Erstens gibt es monomorphe Funktionen. Die Implementierung der Funktion wird durch den Funktionsnamen eindeutig identifiziert. Keiner der Parameter ist speziell.
  • Dann gibt es einen Einzelversand. Einer der Parameter wird als speziell betrachtet und (zusammen mit dem Namen) verwendet, um zu identifizieren, welche Implementierung verwendet werden soll. In OOP neigen wir dazu, diesen Parameter als "das Objekt" zu betrachten, ihn vor dem Funktionsnamen aufzulisten usw.
  • Dann erfolgt der Mehrfachversand. Alle Parameter tragen dazu bei, die zu verwendende Implementierung zu identifizieren. Daher muss auch hier keiner der Parameter speziell sein.

OOP ist offensichtlich mehr als eine Ausrede, einen Parameter als speziell zu nominieren, aber das ist ein Teil davon. Und zurück zu dem, was ich über Kompromisse gesagt habe: Einzelversand ist recht einfach und effizient durchzuführen (die übliche Implementierung wird als "virtuelle Tabellen" bezeichnet). Mehrfachversand ist nicht nur in Bezug auf die Effizienz, sondern auch für die separate Zusammenstellung umständlicher. Wenn Sie neugierig sind, können Sie "das Ausdrucksproblem" nachschlagen.

So wie es etwas seltsam ist, den Begriff "frühe Bindung" für Nichtmitgliedsfunktionen zu verwenden, ist es etwas seltsam, die Begriffe "Einzelversand" und "Mehrfachversand" zu verwenden, bei denen der Polymorphismus zur Kompilierungszeit aufgelöst wird. Normalerweise wird davon ausgegangen, dass C ++ keinen Mehrfachversand aufweist, was als eine bestimmte Art der Laufzeitauflösung angesehen wird. Das Überladen von Funktionen kann jedoch als Mehrfachversand zur Kompilierungszeit angesehen werden.

Zurück zum parametrischen vs. Ad-hoc-Polymorphismus: Diese Begriffe sind in der funktionalen Programmierung beliebter und funktionieren in C ++ nicht ganz. Sogar so...

Parametrischer Polymorphismus bedeutet, dass Sie Typen als Parameter haben und genau derselbe Code verwendet wird, unabhängig davon, welchen Typ Sie für diese Parameter verwenden.

Ad-hoc-Polymorphismus ist Ad-hoc in dem Sinne, dass Sie je nach Typ unterschiedlichen Code bereitstellen.

Überladung und virtuelle Funktionen sind Beispiele für Ad-hoc-Polymorphismus.

Wieder gibt es einige Synonyme ...

|------------+---------------|
| parametric | unconstrained |
| ad-hoc     | constrained   |
|------------+---------------|

Abgesehen davon, dass dies keine Synonyme sind, obwohl sie häufig so behandelt werden, als ob sie es wären, und dass hier in C ++ wahrscheinlich Verwirrung entsteht.

Der Grund dafür, diese als Synonyme zu behandeln, besteht darin, dass durch die Beschränkung des Polymorphismus auf bestimmte Klassen von Typen die Verwendung von Operationen möglich wird, die für diese Klassen von Typen spezifisch sind. Das Wort "Klassen" kann hier im OOP-Sinne interpretiert werden, bezieht sich jedoch nur auf (normalerweise benannte) Sätze von Typen, die bestimmte Operationen gemeinsam haben.

Daher wird parametrischer Polymorphismus normalerweise (zumindest standardmäßig) verwendet, um einen uneingeschränkten Polymorphismus zu implizieren. Da unabhängig von den Typparametern derselbe Code verwendet wird, können nur Operationen unterstützt werden, die für alle Typen funktionieren. Indem Sie die Anzahl der Typen nicht einschränken, schränken Sie die Anzahl der Operationen, die Sie auf diese Typen anwenden können, stark ein.

In zB Haskell können Sie haben ...

myfunc1 :: Bool -> a -> a -> a
myfunc1 c x y = if c then x else y

Das ahier ist ein uneingeschränkter polymorpher Typ. Es könnte alles sein, also können wir mit Werten dieses Typs nicht viel anfangen.

myfunc2 :: Num a => a -> a
myfunc2 x = x + 3

Hier aist er darauf beschränkt, Mitglied der NumKlassentypen zu sein, die sich wie Zahlen verhalten. Mit dieser Einschränkung können Sie mit diesen Werten nummerierte Dinge tun, z. B. sie hinzufügen. Sogar die 3Inferenz 3vom Typ Polymorph stellt fest, dass Sie den Typ meinen a.

Ich betrachte dies als eingeschränkten parametrischen Polymorphismus. Es gibt nur eine Implementierung, die jedoch nur in eingeschränkten Fällen angewendet werden kann. Der Ad-hoc-Aspekt ist die Auswahl +und 3Verwendung. Jede "Instanz" von Numhat ihre eigene Implementierung. Selbst in Haskell sind "parametrisch" und "ungezwungen" keine Synonyme - beschuldigen Sie mich nicht, es ist nicht meine Schuld!

In C ++ sind sowohl Überladung als auch virtuelle Funktionen Ad-hoc-Polymorphismus. Die Definition des Ad-hoc-Polymorphismus spielt keine Rolle, ob die Implementierung zur Laufzeit oder zur Kompilierungszeit ausgewählt wird.

C ++ kommt dem parametrischen Polymorphismus mit Vorlagen sehr nahe, wenn jeder Vorlagenparameter einen Typ hat typename. Es gibt Typparameter und eine einzige Implementierung, unabhängig davon, welche Typen verwendet werden. Die Regel "Substitutionsfehler ist kein Fehler" bedeutet jedoch, dass implizite Einschränkungen durch die Verwendung von Operationen in der Vorlage entstehen. Zusätzliche Komplikationen sind die Vorlagenspezialisierung für die Bereitstellung alternativer Vorlagen - verschiedene (Ad-hoc-) Implementierungen.

In gewisser Weise hat C ++ einen parametrischen Polymorphismus, der jedoch implizit eingeschränkt ist und durch Ad-hoc-Alternativen überschrieben werden kann - dh diese Klassifizierung funktioniert für C ++ nicht wirklich.

Steve314
quelle
+1 Viele interessante Punkte und Erkenntnisse. Ich habe nur ein paar Stunden damit verbracht, über Haskell zu lesen. " aHier ist ein [...] uneingeschränkter polymorpher Typ, daher können wir mit Werten dieses Typs nicht viel anfangen." war von Interesse - in C ++ sans Concepts sind Sie nicht darauf beschränkt, nur einen bestimmten Satz von Operationen mit einem Argument eines als Vorlagenparameter angegebenen Typs zu versuchen ... Bibliotheken wie Boost-Konzepte funktionieren umgekehrt - stellen Sie sicher, dass der Typ Operationen unterstützt Sie geben an, anstatt sich vor versehentlichem Einsatz zusätzlicher Vorgänge zu schützen.
Tony Delroy
@Tony - Konzepte sind eine Möglichkeit, den Polymorphismus von Vorlagen explizit einzuschränken. Die impliziten Einschränkungen werden aufgrund der Kompatibilität offensichtlich nicht verschwinden, aber explizite Einschränkungen werden die Dinge definitiv erheblich verbessern. Ich bin mir ziemlich sicher, dass einige frühere Pläne für Konzepte etwas mit Haskell-Typklassen zu tun hatten, obwohl ich sie nicht so gründlich untersucht habe und als ich das letzte Mal "flach" geschaut habe, wusste ich nicht viel über Haskell.
Steve314
"Die impliziten Einschränkungen werden offensichtlich aus Kompatibilitätsgründen nicht verschwinden" - aus dem Speicher haben C ++ 0x-Konzepte "implizite Einschränkungen" verhindert (versprochen: - /) - Sie konnten den Typ nur auf die von den Konzepten versprochene Weise verwenden.
Tony Delroy
2

Ad-hoc-Polymorphismus bedeutet Funktionsüberladung oder Operatorüberladung. Hier geht's:

http://en.wikipedia.org/wiki/Ad-hoc_polymorphism

In Bezug auf den parametrischen Polymorphismus können Vorlagenfunktionen auch berücksichtigt werden, da sie nicht unbedingt Parameter von FIXED-Typen berücksichtigen. Beispielsweise kann eine Funktion ein Array von Ganzzahlen sortieren und sie kann auch ein Array von Zeichenfolgen usw. sortieren.

http://en.wikipedia.org/wiki/Parametric_polymorphism

Eric Z.
quelle
1
Leider ist dies irreführend, obwohl es richtig ist. Vorlagenfunktionen können aufgrund der SFINAE-Regel implizite Einschränkungen erhalten - die Verwendung einer Operation innerhalb der Vorlage schränkt den Polymorphismus implizit ein - und die Vorlagenspezialisierung kann alternative Ad-hoc-Vorlagen bereitstellen, die die allgemeineren Vorlagen überschreiben. Eine Vorlage bietet (standardmäßig) einen uneingeschränkten parametrischen Polymorphismus, aber es gibt keine Durchsetzung dafür - es gibt mindestens zwei Möglichkeiten, wie sie eingeschränkt oder ad-hoc werden kann.
Steve314
Tatsächlich impliziert Ihr Beispiel - Sortieren - eine Einschränkung. Das Sortieren funktioniert nur für Typen, die geordnet sind (dh die <und ähnliche Operatoren bereitstellen ). In Haskell würden Sie diese Anforderung explizit mit der Klasse ausdrücken Ord. Die Tatsache, dass Sie <je nach Typ (wie von der Instanz von angegeben Ord) unterschiedliche Werte erhalten, wird als Ad-hoc-Polymorphismus betrachtet.
Steve314
2

Dies mag nicht hilfreich sein, aber ich habe dies gemacht, um meine Freunde in die Programmierung einzuführen, indem ich definierte Funktionen wie STARTund ENDfür die Hauptfunktion herausgab, damit es nicht zu entmutigend war (sie verwendeten nur die Datei main.cpp ). Es enthält polymorphe Klassen und Strukturen, Vorlagen, Vektoren, Arrays, Präprozessor-Direktiven, Freundschaften, Operatoren und Zeiger (die Sie wahrscheinlich alle kennen sollten, bevor Sie Polymorphismus versuchen):

Hinweis: Es ist noch nicht fertig, aber Sie können sich ein Bild machen

main.cpp

#include "main.h"
#define ON_ERROR_CLEAR_SCREEN false
START
    Library MyLibrary;
    Book MyBook("My Book", "Me");
    MyBook.Summarize();
    MyBook += "Hello World";
    MyBook += "HI";
    MyBook.EditAuthor("Joe");
    MyBook.EditName("Hello Book");
    MyBook.Summarize();
    FixedBookCollection<FairyTale> FBooks("Fairytale Books");
    FairyTale MyTale("Tale", "Joe");
    FBooks += MyTale;
    BookCollection E("E");
    MyLibrary += E;
    MyLibrary += FBooks;
    MyLibrary.Summarize();
    MyLibrary -= FBooks;
    MyLibrary.Summarize();
    FixedSizeBookCollection<5> Collection("My Fixed Size Collection");
    /* Extension Work */ Book* Duplicate = MyLibrary.DuplicateBook(&MyBook);
    /* Extension Work */ Duplicate->Summarize();
END

main.h

#include <iostream>
#include <sstream>
#include <vector>
#include <string>
#include <type_traits>
#include <array>
#ifndef __cplusplus
#error Not C++
#endif
#define START int main(void)try{
#define END GET_ENTER_EXIT return(0);}catch(const std::exception& e){if(ON_ERROR_CLEAR_SCREEN){system("cls");}std::cerr << "Error: " << e.what() << std::endl; GET_ENTER_EXIT return (1);}
#define GET_ENTER_EXIT std::cout << "Press enter to exit" << std::endl; getchar();
class Book;
class Library;
typedef std::vector<const Book*> Books;
bool sContains(const std::string s, const char c){
    return (s.find(c) != std::string::npos);
}
bool approve(std::string s){
    return (!sContains(s, '#') && !sContains(s, '%') && !sContains(s, '~'));
}
template <class C> bool isBook(){
    return (typeid(C) == typeid(Book) || std::is_base_of<Book, C>());
}
template<class ClassToDuplicate> class DuplicatableClass{ 
public:
    ClassToDuplicate* Duplicate(ClassToDuplicate ToDuplicate){
        return new ClassToDuplicate(ToDuplicate);
    }
};
class Book : private DuplicatableClass<Book>{
friend class Library;
friend struct BookCollection;
public:
    Book(const char* Name, const char* Author) : name_(Name), author_(Author){}
    void operator+=(const char* Page){
        pages_.push_back(Page);
    }
    void EditAuthor(const char* AuthorName){
        if(approve(AuthorName)){
            author_ = AuthorName;
        }
        else{
            std::ostringstream errorMessage;
            errorMessage << "The author of the book " << name_ << " could not be changed as it was not approved";
            throw std::exception(errorMessage.str().c_str());
        }
    }
    void EditName(const char* Name){
        if(approve(Name)){
            name_ = Name;
        }
        else{
            std::ostringstream errorMessage;
            errorMessage << "The name of the book " << name_ << " could not be changed as it was not approved";
            throw std::exception(errorMessage.str().c_str());
        }
    }
    virtual void Summarize(){
        std::cout << "Book called " << name_ << "; written by " << author_ << ". Contains "
            << pages_.size() << ((pages_.size() == 1) ? " page:" : ((pages_.size() > 0) ? " pages:" : " pages")) << std::endl;
        if(pages_.size() > 0){
            ListPages(std::cout);
        }
    }
private:
    std::vector<const char*> pages_;
    const char* name_;
    const char* author_;
    void ListPages(std::ostream& output){
        for(int i = 0; i < pages_.size(); ++i){
            output << pages_[i] << std::endl;
        }
    }
};
class FairyTale : public Book{
public:
    FairyTale(const char* Name, const char* Author) : Book(Name, Author){}
};
struct BookCollection{
friend class Library;
    BookCollection(const char* Name) : name_(Name){}
    virtual void operator+=(const Book& Book)try{
        Collection.push_back(&Book); 
    }catch(const std::exception& e){
        std::ostringstream errorMessage;
        errorMessage << e.what() << " - on line (approx.) " << (__LINE__ -3);
        throw std::exception(errorMessage.str().c_str());
    }
    virtual void operator-=(const Book& Book){
        for(int i = 0; i < Collection.size(); ++i){
            if(Collection[i] == &Book){
                Collection.erase(Collection.begin() + i);
                return;
            }
        }
        std::ostringstream errorMessage;
        errorMessage << "The Book " << Book.name_ << " was not found, and therefore cannot be erased";
        throw std::exception(errorMessage.str().c_str());
    }
private:
    const char* name_;
    Books Collection;
};
template<class FixedType> struct FixedBookCollection : public BookCollection{
    FixedBookCollection(const char* Name) : BookCollection(Name){
        if(!isBook<FixedType>()){
            std::ostringstream errorMessage;
            errorMessage << "The type " << typeid(FixedType).name() << " cannot be initialized as a FixedBookCollection";
            throw std::exception(errorMessage.str().c_str());
            delete this;
        }
    }
    void operator+=(const FixedType& Book)try{
        Collection.push_back(&Book); 
    }catch(const std::exception& e){
        std::ostringstream errorMessage;
        errorMessage << e.what() << " - on line (approx.) " << (__LINE__ -3);
        throw std::exception(errorMessage.str().c_str());
    }
    void operator-=(const FixedType& Book){
        for(int i = 0; i < Collection.size(); ++i){
            if(Collection[i] == &Book){
                Collection.erase(Collection.begin() + i);
                return;
            }
        }
        std::ostringstream errorMessage;
        errorMessage << "The Book " << Book.name_ << " was not found, and therefore cannot be erased";
        throw std::exception(errorMessage.str().c_str());
    }
private:
    std::vector<const FixedType*> Collection;
};
template<size_t Size> struct FixedSizeBookCollection : private std::array<const Book*, Size>{
    FixedSizeBookCollection(const char* Name) : name_(Name){ if(Size < 1){ throw std::exception("A fixed size book collection cannot be smaller than 1"); currentPos = 0; } }
    void operator+=(const Book& Book)try{
        if(currentPos + 1 > Size){
            std::ostringstream errorMessage;
            errorMessage << "The FixedSizeBookCollection " << name_ << "'s size capacity has been overfilled";
            throw std::exception(errorMessage.str().c_str());
        }
        this->at(currentPos++) = &Book;
    }catch(const std::exception& e){
        std::ostringstream errorMessage;
        errorMessage << e.what() << " - on line (approx.) " << (__LINE__ -3);
        throw std::exception(errorMessage.str().c_str());
    }
private:
    const char* name_;
    int currentPos;
};
class Library : private std::vector<const BookCollection*>{
public:
    void operator+=(const BookCollection& Collection){
        for(int i = 0; i < size(); ++i){
            if((*this)[i] == &Collection){
                std::ostringstream errorMessage;
                errorMessage << "The BookCollection " << Collection.name_ << " was already in the library, and therefore cannot be added";
                throw std::exception(errorMessage.str().c_str());
            }
        }
        push_back(&Collection);
    }
    void operator-=(const BookCollection& Collection){
        for(int i = 0; i < size(); ++i){
            if((*this)[i] == &Collection){
                erase(begin() + i);
                return;
            }
        }
        std::ostringstream errorMessage;
        errorMessage << "The BookCollection " << Collection.name_ << " was not found, and therefore cannot be erased";
        throw std::exception(errorMessage.str().c_str());
    }
    Book* DuplicateBook(Book* Book)const{
        return (Book->Duplicate(*Book));
    }
    void Summarize(){
        std::cout << "Library, containing " << size() << ((size() == 1) ? " book collection:" : ((size() > 0) ? " book collections:" : " book collections")) << std::endl;
        if(size() > 0){
            for(int i = 0; i < size(); ++i){
                std::cout << (*this)[i]->name_ << std::endl;
            }
        }
    }
};
Joe
quelle
1

Hier ist ein grundlegendes Beispiel für die Verwendung polymorpher Klassen

#include <iostream>

class Animal{
public:
   Animal(const char* Name) : name_(Name){/* Add any method you would like to perform here*/
    virtual void Speak(){
        std::cout << "I am an animal called " << name_ << std::endl;
    }
    const char* name_;
};

class Dog : public Animal{
public:
    Dog(const char* Name) : Animal(Name) {/*...*/}
    void Speak(){
        std::cout << "I am a dog called " << name_ << std::endl;
    }
};

int main(void){
    Animal Bob("Bob");
    Dog Steve("Steve");
    Bob.Speak();
    Steve.Speak();
    //return (0);
}
user2976089
quelle
0

Polymorphismus bedeutet viele Formen als solche, die ein Operator verwendet, um unter verschiedenen Instanzen unterschiedlich zu handeln. Polymorphismus wird verwendet, um Vererbung zu implementieren. Zum Beispiel haben wir ein fn draw () für eine Klassenform definiert, dann kann das Zeichnen fn zum Zeichnen von Kreisen, Kästchen, Dreiecken und anderen Formen implementiert werden. (die Objekte der Klassenform sind)

Jayraj Srikriti Naidu
quelle
-3

Wenn jemand diesen Leuten CUT sagt

The Surgeon
The Hair Stylist
The Actor

Was wird passieren?

The Surgeon would begin to make an incision.
The Hair Stylist would begin to cut someone's hair.
The Actor would abruptly stop acting out of the current scene, awaiting directorial guidance.

Die obige Darstellung zeigt also, was Polymorphismus (gleicher Name, unterschiedliches Verhalten) in OOP ist.

Wenn Sie zu einem Interview gehen und der Interviewer Sie bittet, ein Live-Beispiel für Polymorphismus in demselben Raum zu erzählen / zu zeigen, in dem wir sitzen, sagen wir:

Antwort - Tür / Fenster

Fragen Sie sich, wie?

Durch Tür / Fenster - eine Person kann kommen, Luft kann kommen, Licht kann kommen, Regen kann kommen usw.

dh man bildet ein anderes Verhalten (Polymorphismus).

Um es besser und auf einfache Weise zu verstehen, habe ich das obige Beispiel verwendet. Wenn Sie eine Referenz für den Code benötigen, folgen Sie den obigen Antworten.

Sanchit
quelle
Wie ich zum besseren Verständnis des Polymorphismus in c ++ erwähnt habe, habe ich das obige Beispiel verwendet. Dies kann einem Neuling helfen, tatsächlich zu verstehen und zu beschreiben, was die Bedeutung ist oder was hinter dem Code passiert, während er beim Interview auftritt. Danke dir!
Sanchit
op fragte "Polymorphismus in c ++". Ihre Antwort ist viel zu abstrakt.
StahlRat