web-dev-qa-db-fra.com

Polymorphisme en C ++

AUTANT QUE JE SACHE:

C++ fournit trois types différents de polymorphisme.

  • Fonctions virtuelles
  • Nom de fonction en surcharge
  • Surcharge de l'opérateur

Outre les trois types de polymorphisme ci-dessus, il existe d'autres types de polymorphisme:

  • temps d'exécution
  • temps de compilation
  • polymorphisme ad hoc
  • polymorphisme paramétrique

Je sais que polymorphisme d'exécution peut être atteint avec fonctions virtuelles et polymorphisme statique peut être obtenu avec template fonctions

Mais pour les deux autres

polymorphisme ad-hoc:

Si la gamme des types réels pouvant être utilisés est finie et que les combinaisons doivent être spécifiées individuellement avant utilisation, on parle de polymorphisme ad hoc.

polymorphisme paramétrique:

Si tout le code est écrit sans mention de type spécifique et peut donc être utilisé de manière transparente avec un nombre quelconque de nouveaux types, il est appelé polymorphisme paramétrique.

Je peux à peine les comprendre :(

quelqu'un peut-il expliquer les deux si possible avec un exemple? J'espère que les réponses à ces questions seront utiles pour de nombreux nouveaux arrivants de leurs collèges.

123
Vijay

Compréhension/exigences du polymorphisme

Pour comprendre le polymorphisme - tel que ce terme est utilisé en informatique - il est utile de commencer par un simple test et sa définition. Considérer:

    Type1 x;
    Type2 y;

    f(x);
    f(y);

Ici, f() doit exécuter certaines opérations et reçoit les valeurs x et y en tant qu'entrées.

Pour présenter un polymorphisme, f() doit pouvoir fonctionner avec des valeurs d'au moins deux types distincts (par exemple, int et double. ), trouver et exécuter un code distinct adapté au type.


Mécanismes C++ pour le polymorphisme

Polymorphisme explicite spécifié par le programmeur

Vous pouvez écrire f() de telle sorte qu'il puisse fonctionner sur plusieurs types de l'une des manières suivantes:

  • Prétraitement:

    #define f(X) ((X) += 2)
    // (note: in real code, use a longer uppercase name for a macro!)
    
  • Surcharge:

    void f(int& x)    { x += 2; }
    
    void f(double& x) { x += 2; }
    
  • Modèles:

    template <typename T>
    void f(T& x) { x += 2; }
    
  • Envoi virtuel:

    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
    

Autres mécanismes connexes

Le polymorphisme fourni par le compilateur pour les types prédéfinis, les conversions standard et le transtypage/la coercition est présenté plus loin comme complément:

  • de toute façon, ils sont généralement compris intuitivement (ce qui justifie une réaction du type "oh, that"),
  • ils ont un impact sur le seuil requis pour exiger, et sur une utilisation transparente, les mécanismes ci-dessus, et
  • l'explication est une distraction fastidieuse de concepts plus importants.

Terminologie

Catégorisation supplémentaire

Compte tenu des mécanismes polymorphes ci-dessus, nous pouvons les catégoriser de différentes manières:

  • Quand le code polymorphe spécifique au type est-il sélectionné?

    • Run time signifie que le compilateur doit générer du code pour tous les types que le programme peut gérer lors de son exécution. Au moment de l'exécution, le code correct est sélectionné (virtual dispatch =)
    • Le temps de compilation signifie que le choix du code spécifique au type est effectué lors de la compilation. Une conséquence de ceci: disons un programme appelé uniquement f ci-dessus avec des arguments int - selon le mécanisme polymorphe utilisé et les choix en ligne, le compilateur pourrait éviter de générer du code pour f(double), ou le code généré peut être jeté à un moment donné lors de la compilation ou de la liaison. (tous les mécanismes ci-dessus sauf la répartition virtuelle)

  • Quels types sont supportés?

    • Ad-hoc signifie que vous fournissez un code explicite pour prendre en charge chaque type (par exemple, surcharge, spécialisation de modèle); vous ajoutez explicitement le support "pour ce" (selon ad hoc signification du =), un autre "this", et peut-être "ça" aussi ;-).
    • Paramétrique signifie que vous pouvez simplement essayer d'utiliser la fonction pour différents types de paramètres sans rien faire de particulier pour permettre sa prise en charge (par exemple, des modèles, des macros). Un objet avec des fonctions/opérateurs qui agissent comme le modèle/la macro attend1 is tout ce que le modèle/macro doit faire son travail, le type exact n'étant pas pertinent. Les "concepts" extraits de C++ 11 aident à exprimer et à appliquer de telles attentes - espérons qu'ils en feront une norme ultérieure.

      • Le polymorphisme paramétrique fournit la frappe de canard - un concept attribué à James Whitcomb Riley qui a apparemment dit "Quand je vois un oiseau qui marche comme un canard et qui nage comme un canard et des charlatans comme un canard, j’appelle cet oiseau canard. ".

        template <typename Duck>
        void do_ducky_stuff(const Duck& x) { x.walk().swim().quack(); }
        
        do_ducky_stuff(Vilified_Cygnet());
        
    • polymorphisme de sous-type (ou inclusion) vous permet de travailler sur de nouveaux types sans mettre à jour l'algorithme/la fonction, mais ils doivent être dérivés de la même classe de base (envoi virtuel)

1 - Les modèles sont extrêmement flexibles. SFINAE (voir aussi std::enable_if ) permet effectivement plusieurs ensembles d'attentes pour le polymorphisme paramétrique. Par exemple, vous pouvez indiquer que lorsque le type de données que vous traitez a un membre .size(), vous utilisez une fonction, sinon une autre fonction qui n'a pas besoin de .size() (mais souffre vraisemblablement en quelque sorte - par exemple en utilisant le plus lent strlen() ou en n’imprimant pas un message utile dans le journal). Vous pouvez également spécifier des comportements ad-hoc lorsque le modèle est instancié avec des paramètres spécifiques, en laissant certains paramètres paramétriques ( spécialisation de modèle partielle ) ou non ( - spécialisation complète ).

"Polymorphe"

Alf Steinbach remarque que, dans la norme C++ , polymorphe fait uniquement référence au polymorphisme au moment de l'exécution utilisant la répartition virtuelle. Général Comp. Sci. la signification est plus inclusive, selon le glossaire du créateur de C++, Bjarne Stroustrup ( http://www.stroustrup.com/glossary.html ):

polymorphisme - fournissant une interface unique à des entités de types différents. Les fonctions virtuelles fournissent un polymorphisme dynamique (à l'exécution) via une interface fournie par une classe de base. Les fonctions et les modèles surchargés fournissent un polymorphisme statique (à la compilation). TC++ PL 12.2.6, 13.6.1, D & E 2.9.

Cette réponse - comme la question - relie les fonctionnalités C++ à la Comp. Sci. terminologie.

Discussion

Avec la norme C++ utilisant une définition plus étroite du "polymorphisme" que celle de Comp. Sci. communauté, pour assurer la compréhension mutuelle pour votre audience considère ...

  • en utilisant une terminologie non ambiguë ("pouvons-nous rendre ce code réutilisable pour d'autres types?" ou "pouvons-nous utiliser l'envoi virtuel?" plutôt que "pouvons-nous rendre ce code polymorphe?"), et/ou
  • définissant clairement votre terminologie.

Néanmoins, ce qui est crucial pour être un bon programmeur C++, c'est comprendre ce que le polymorphisme fait vraiment pour vous ...

vous permettant d'écrire du code "algorithmique" une fois puis de l'appliquer à de nombreux types de données

... et ensuite, soyez très conscient de la manière dont différents mécanismes polymorphes répondent à vos besoins réels.

Le polymorphisme d'exécution convient à:

  • entrée traitée par les méthodes d'usine et crachée en tant que collection d'objets hétérogènes gérée via Base* s,
  • implémentation choisie au moment de l'exécution en fonction des fichiers de configuration, des commutateurs de ligne de commande, des paramètres de l'interface utilisateur, etc.,
  • la mise en œuvre variait au moment de l'exécution, par exemple pour un modèle de machine à états.

Lorsqu'il n'y a pas de pilote clair pour le polymorphisme au moment de l'exécution, les options de compilation sont souvent préférables. Considérer:

  • l'aspect compilation de ce que l'on appelle des classes basées sur des modèles est préférable aux interfaces fat qui échouent au moment de l'exécution.
  • SFINAE
  • CRTP
  • optimisations (nombreuses, y compris l'élimination du code mort et en ligne, le déroulement de la boucle, les tableaux statiques basés sur des piles et les tas)
  • __FILE__, __LINE__, Concaténation littérale de chaîne et autres fonctionnalités uniques des macros (qui restent diaboliques ;-))
  • les modèles et les macros testent l'utilisation sémantique est prise en charge, mais ne restreignez pas artificiellement la manière dont cette prise en charge est fournie (la répartition virtuelle tend à exiger des correspondances de fonction de membre identiques)

Autres mécanismes supportant le polymorphisme

Comme promis, pour être complet, plusieurs sujets périphériques sont abordés:

  • surcharges fournies par le compilateur
  • conversions
  • castes/coercition

Cette réponse se termine par une discussion sur la manière dont ce qui précède se combine pour renforcer et simplifier le code polymorphe, en particulier le polymorphisme paramétrique (modèles et macros).

Mécanismes de mappage à des opérations spécifiques à un type

> Surcharges implicites fournies par le compilateur

Conceptuellement, le compilateur surcharge plusieurs opérateurs pour les types intégrés. Ce n'est pas conceptuellement différent de la surcharge spécifiée par l'utilisateur, mais il est répertorié car il est facilement négligé. Par exemple, vous pouvez ajouter à ints et doubles en utilisant la même notation x += 2 Et le compilateur produit:

  • instructions de CPU spécifiques au type
  • un résultat du même type.

La surcharge s'étend ensuite de manière transparente aux types définis par l'utilisateur:

std::string x;
int y = 0;

x += 'c';
y += 'c';

Les surcharges fournies par le compilateur pour les types de base sont courantes dans les langages informatiques de haut niveau (3GL +), et une discussion explicite sur le polymorphisme implique généralement quelque chose de plus. (2GLs - Langages d'assemblage - obligent souvent le programmeur à utiliser explicitement différentes mnémoniques pour différents types.)

> Conversions standard

La quatrième section de la norme C++ décrit les conversions standard.

Le premier point résume bien (à partir d'un ancien projet - j'espère toujours substantiellement correct):

-1- Les conversions standard sont des conversions implicites définies pour les types intégrés. La clause convoque l'ensemble complet de ces conversions. Une séquence de conversion standard est une séquence de conversions standard dans l'ordre suivant:

  • Aucune ou une conversion de l'ensemble suivant: conversion de valeur à valeur, conversion de tableau en pointeur et conversion de fonction à pointeur.

  • Zéro ou une conversion de l'ensemble suivant: promotions intégrales, promotion à virgule flottante, conversions intégrales, conversions à virgule flottante, conversions à virgule flottante, conversions de pointeur, conversions pointeur, conversions pointeur à membre et conversions booléennes.

  • Conversion de zéro ou une qualification.

[Remarque: une séquence de conversion standard peut être vide, c’est-à-dire qu’elle ne peut consister en aucune conversion. ] Une séquence de conversion standard sera appliquée à une expression si nécessaire pour la convertir en un type de destination requis.

Ces conversions permettent un code tel que:

double a(double x) { return x + 2; }

a(3.14);
a(42);

Application du test précédent:

Pour être polymorphe, [a()] doit pouvoir fonctionner avec des valeurs d'au moins deux types distincts (par exemple, int et double), recherche et exécution du code approprié au type .

a() lui-même exécute le code spécifiquement pour double et est donc pas polymorphe.

Mais, lors du deuxième appel à a(), le compilateur sait générer un code approprié au type pour une "promotion à virgule flottante" (norme §4) permettant de convertir 42 En 42.0. Ce code supplémentaire est dans la fonction calling. Nous discuterons de l'importance de ceci dans la conclusion.

> Coercition, transtypes, constructeurs implicites

Ces mécanismes permettent aux classes définies par l'utilisateur de spécifier des comportements similaires aux conversions standard des types intégrés. Regardons:

int a, b;

if (std::cin >> a >> b)
    f(a, b);

Ici, l'objet std::cin Est évalué dans un contexte booléen, à l'aide d'un opérateur de conversion. Cela peut être groupé conceptuellement avec les "promotions intégrales" et autres des conversions standard du sujet ci-dessus.

Les constructeurs implicites font effectivement la même chose, mais sont contrôlés par le type cast-to:

f(const std::string& x);
f("hello");  // invokes `std::string::string(const char*)`

Conséquences des surcharges, des conversions et de la contrainte imposées par le compilateur

Considérer:

void f()
{
    typedef int Amount;
    Amount x = 13;
    x /= 2;
    std::cout << x * 1.1;
}

Si nous voulons que le montant x soit traité comme un nombre réel lors de la division (c'est-à-dire soit 6,5 plutôt que arrondi à 6), nous seulement ​​devons passer à typedef double Amount.

C'est bien, mais cela n'aurait pas été trop beaucoup de travail pour rendre le code explicitement "taper correctement":

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;
}                                      }

Mais considérons que nous pouvons transformer la première version en un template:

template <typename Amount>
void f()
{
    Amount x = 13;
    x /= 2;
    std::cout << x * 1.1;
}

C’est grâce à ces petites "fonctionnalités pratiques" qu’il peut être si facilement instancié pour int ou double et fonctionner comme prévu. Sans ces fonctionnalités, nous aurions besoin de transtypages explicites, de traits de type et/ou de classes de règles, ainsi que des erreurs verbeuses et sujettes aux erreurs telles que

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;
}

Ainsi, la surcharge fournie par le compilateur pour les types intégrés, les conversions standard, les constructeurs casting/coercion/implicites - tout cela contribue subtilement au soutien du polymorphisme. A partir de la définition en haut de cette réponse, ils abordent "la recherche et l'exécution de code approprié au type" en mappant:

  • "loin" des types de paramètres

    • de les nombreux types de données polymorphes manipulés par un code algorithmique

    • to code écrit pour un nombre (potentiellement moindre) de types (identiques ou autres).

  • "to" types paramétriques à partir de valeurs de type constant

Ils établissent pas des contextes polymorphes par eux-mêmes, mais aident à responsabiliser/simplifier le code dans de tels contextes.

Vous pouvez vous sentir trompé ... cela ne semble pas beaucoup. Ce qui est important, c’est que dans les contextes polymorphes paramétriques (c’est-à-dire à l’intérieur de modèles ou de macros), nous essayons de prendre en charge une gamme arbitraire de types, mais nous souhaitons souvent y exprimer des opérations sous la forme d’autres fonctions, littéraux et opérations conçus pour un environnement donné. petit ensemble de types. Cela réduit la nécessité de créer des fonctions ou des données presque identiques pour chaque type lorsque l'opération/la valeur est logiquement identique. Ces fonctionnalités coopèrent pour ajouter une attitude de "meilleur effort", en réalisant ce qui est intuitivement attendu en utilisant les fonctions et les données disponibles limitées, et en s’arrêtant avec une erreur en cas de réelle ambiguïté.

Cela aide à limiter le besoin de code polymorphe prenant en charge le code polymorphe, en définissant un réseau plus étroit autour de l'utilisation du polymorphisme afin que l'utilisation localisée ne force pas l'utilisation généralisée, et en rendant les avantages du polymorphisme disponibles au besoin sans imposer les coûts de la mise en œuvre au moment de la compilation, avoir plusieurs copies de la même fonction logique dans le code objet pour prendre en charge les types utilisés, et en effectuant une répartition virtuelle par opposition aux appels en ligne ou au moins résolus au moment de la compilation. Comme il est typique en C++, le programmeur dispose de beaucoup de liberté pour contrôler les limites dans lesquelles le polymorphisme est utilisé.

212
Tony Delroy

En C++, la distinction importante est la liaison d'exécution au moment de la compilation. Ad-hoc vs paramétrique n'aide pas vraiment, comme je l'expliquerai plus tard.

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

Remarque: le polymorphisme au moment de l'exécution peut toujours être résolu au moment de la compilation, mais il ne s'agit que d'une optimisation. Avoir besoin de prendre en charge efficacement la résolution au moment de l'exécution, et de trouver des solutions à d'autres problèmes, explique en partie pourquoi les fonctions virtuelles sont ce qu'elles sont. Et c’est vraiment essentiel pour toutes les formes de polymorphisme en C++ - chacune découle de différents ensembles de compromis effectués dans un contexte différent.

La surcharge de fonctions et la surcharge d'opérateurs sont la même chose qui compte. Les noms et la syntaxe pour les utiliser n'affectent pas le polymorphisme.

Les modèles vous permettent de spécifier de nombreuses surcharges de fonctions à la fois.

Il y a une autre série de noms pour la même idée de résolution ...

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

Ces noms sont davantage associés à la programmation orientée objet. Il est donc un peu étrange de dire qu'un modèle ou une autre fonction non membre utilise une liaison anticipée.

Pour mieux comprendre la relation entre les fonctions virtuelles et la surcharge de fonctions, il est également utile de comprendre la différence entre "dispatch unique" et "dispatch multiple". L'idée peut être comprise comme une progression ...

  • Premièrement, il y a des fonctions monomorphes. L'implémentation de la fonction est identifiée de manière unique par le nom de la fonction. Aucun des paramètres n'est spécial.
  • Ensuite, il y a une seule dépêche. L'un des paramètres est considéré comme spécial et est utilisé (avec le nom) pour identifier quelle implémentation utiliser. En POO, nous avons tendance à considérer ce paramètre comme "l'objet", à le lister avant le nom de la fonction, etc.
  • Ensuite, il y a plusieurs envois. Tous/tous les paramètres contribuent à identifier quelle implémentation utiliser. Par conséquent, encore une fois, aucun des paramètres ne doit être spécial.

Il ya évidemment plus à OOP qu’une excuse pour désigner un paramètre comme spécial, mais c’est une partie de celle-ci. Pour en revenir à ce que j’ai dit à propos des compromis, il est assez facile de choisir une dépêche. faire efficacement (l’implémentation habituelle est appelée "tables virtuelles"). La répartition multiple est plus délicate, non seulement en termes d’efficacité, mais aussi pour une compilation séparée. Si vous êtes curieux, vous pourriez rechercher le "problème d’expression".

Tout comme il est un peu étrange d'utiliser le terme "liaison anticipée" pour les fonctions non membres, il est un peu étrange d'utiliser les termes "envoi simple" et "envoi multiple" dans lesquels le polymorphisme est résolu au moment de la compilation. En règle générale, on considère que C++ n'a pas plusieurs envois, ce qui est considéré comme un type particulier de résolution à l'exécution. Cependant, la surcharge de fonctions peut être vue comme une distribution multiple effectuée au moment de la compilation.

Pour en revenir au polymorphisme paramétrique vs ad-hoc, ces termes sont plus populaires en programmation fonctionnelle et ne fonctionnent pas tout à fait en C++. Toutefois...

Polymorphisme paramétrique signifie que vous avez des types en tant que paramètres et que le même code est utilisé quel que soit le type utilisé pour ces paramètres.

Le polymorphisme ad hoc est ad hoc dans le sens où vous fournissez un code différent selon les types.

La surcharge et les fonctions virtuelles sont deux exemples de polymorphisme ad hoc.

Encore une fois, il y a des synonymes ...

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

Sauf que ce ne sont pas vraiment des synonymes, bien qu'ils soient généralement traités comme s'ils l'étaient, et c'est là que la confusion risque de survenir en C++.

Le traitement qui en est fait est que, en contraignant le polymorphisme à des classes de types particulières, il devient possible d'utiliser des opérations spécifiques à ces classes de types. Le mot "classes" peut ici être interprété dans le sens OOP, mais ne fait en réalité que se rapporter à des ensembles (généralement nommés) de types qui partagent certaines opérations.

Ainsi, le polymorphisme paramétrique est généralement pris (du moins par défaut) comme impliquant un polymorphisme non contraint. Comme le même code est utilisé quels que soient les paramètres de type, les seules opérations pouvant être prises en charge sont celles qui fonctionnent pour tous les types. En laissant l'ensemble de types non contraint, vous limitez sévèrement l'ensemble des opérations que vous pouvez appliquer à ces types.

Dans par exemple Haskell, vous pouvez avoir ...

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

Le a ici est un type polymorphe non contraint. Cela pourrait être n'importe quoi, alors nous ne pouvons pas faire grand chose avec des valeurs de ce type.

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

Ici, a est contraint d'être membre de la classe Num - des types qui agissent comme des nombres. Cette contrainte vous permet d'effectuer des opérations numérotées avec ces valeurs, telles que les ajouter. Même le 3 est polymorphe - l’inférence de type comprend que vous voulez dire le 3 de type a.

Je pense à cela comme à un polymorphisme paramétrique contraint. Il n'y a qu'une seule implémentation, mais elle ne peut être appliquée que dans des cas limités. L'aspect ad-hoc est le choix de quel + et 3 utiliser. Chaque "instance" de Num a sa propre implémentation distincte. Donc, même en Haskell, "paramétrique" et "sans contrainte" ne sont pas vraiment synonymes - ne me blâmez pas, ce n'est pas de ma faute!

En C++, la surcharge et les fonctions virtuelles sont un polymorphisme ad hoc. La définition du polymorphisme ad hoc ne tient pas compte du fait que l'implémentation est sélectionnée au moment de l'exécution ou de la compilation.

C++ se rapproche beaucoup du polymorphisme paramétrique avec les modèles si chaque paramètre de modèle est de type typename. Il existe des paramètres de type et une seule implémentation, quels que soient les types utilisés. Toutefois, la règle "L'échec de la substitution n'est pas une erreur" signifie que des contraintes implicites résultent de l'utilisation d'opérations dans le modèle. Des complications supplémentaires incluent la spécialisation de modèles pour fournir des modèles alternatifs - différentes implémentations (ad-hoc).

Donc, d’une certaine manière, le C++ a un polymorphisme paramétrique, mais il est implicitement contraint et peut être remplacé par des alternatives ad-hoc - c’est-à-dire que cette classification ne fonctionne pas vraiment pour C++.

14
Steve314

Cela n’aidera peut-être pas, mais j’ai fait cela pour initier mes amis à la programmation en donnant des fonctions définies, comme START et END pour la fonction principale, de sorte que ce n’était pas trop intimidant ( ils ont uniquement utilisé le fichier main.cpp ). Il contient des classes et des structures polymorphes, des modèles, des vecteurs, des tableaux, des directives préprocesseurs, des liens d'amitié, des opérateurs et des pointeurs (que vous devriez probablement tous connaître avant d'essayer le polymorphisme):

Note: Ce n'est pas fini, mais vous pouvez avoir l'idée

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;
            }
        }
    }
};
2
Joe

En ce qui concerne le polymorphisme ad hoc, cela signifie une surcharge de fonctions ou une surcharge d’opérateurs. Découvrez ici:

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

En ce qui concerne le polymorphisme paramétrique, les fonctions de modèle peuvent également être comptabilisées car elles ne prennent pas nécessairement en compte les paramètres de types FIXED. Par exemple, une fonction peut trier un tableau d'entiers et elle peut également trier un tableau de chaînes, etc.

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

2
Eric Z

Voici un exemple de base utilisant des classes polymorphes

#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);
}
1
user2976089

Polymorphisme signifie beaucoup de formes en tant que telles, il est utilisé pour qu'un opérateur agisse différemment dans différentes instances. Le polymorphisme est utilisé pour implémenter l'héritage. Par exemple, nous avons défini un draw fn () pour une forme de classe, puis le draw fn peut être utilisé pour dessiner des cercles, des boîtes, des triangles et d’autres formes. (qui sont des objets de la forme de classe)

0