Polymorphisme en C ++

129

AUTANT QUE JE SACHE:

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

  • Fonctions virtuelles
  • Surcharge du nom de la fonction
  • Surcharge de l'opérateur

En plus des trois types de polymorphisme ci-dessus, il existe d'autres types de polymorphisme:

  • Durée
  • au moment de la compilation
  • polymorphisme ad hoc
  • polymorphisme paramétrique

Je sais que le polymorphisme d'exécution peut être réalisé par des fonctions virtuelles et que le polymorphisme statique peut être réalisé par des fonctions de modèle

Mais pour les deux autres

  • polymorphisme ad hoc
  • polymorphisme paramétrique dit le site Web ,

polymorphisme ad hoc:

Si la gamme de types réels qui peuvent ê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 d'un 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 évanouissements de leurs collèges.

Vijay
la source
30
En fait, C ++ a quatre types de polymorphisme: paramétrique (généricité via des modèles en C ++), inclusion (sous-typage via des méthodes virtuelles en C ++), surcharge et coercition (conversions implicites). Sur le plan de la conception, il y a peu de distinction entre la surcharge de fonctions et la surcharge d'opérateur.
fredoverflow
Donc, il semble que le site Web que j'ai mentionné en trompe beaucoup .. ai-je raison?
Vijay
@zombie: ce site Web aborde beaucoup de bons concepts, mais n'est pas précis et cohérent dans son utilisation de la terminologie (par exemple, une fois qu'il commence à parler de polymorphisme de répartition virtuelle / d'exécution, il fait beaucoup de déclarations erronées sur le polymorphisme en général mais vrai pour l'envoi virtuel). Si vous comprenez déjà le sujet, vous pouvez vous rapporter à ce qui est dit et insérer mentalement les mises en garde nécessaires, mais il est difficile d'y arriver en lisant le site ....
Tony Delroy
Certains termes sont quasi-synonymes, ou plus liés à mais plus restreints que d'autres termes. Par exemple, le terme «polymorphisme ad hoc» est principalement utilisé dans Haskell dans mon expérience, mais les «fonctions virtuelles» sont très étroitement liées. La différence mineure est que «fonctions virtuelles» est un terme orienté objet faisant référence aux fonctions membres avec «liaison tardive». La "distribution multiple" est également une sorte de polymorphisme ad hoc. Et comme le dit FredOverflow, la surcharge d'opérateurs et de fonctions est fondamentalement la même chose.
Steve314
J'ai corrigé votre mise en forme pour vous. Veuillez lire l'aide disponible à droite du volet d'édition. Quelqu'un avec plus de 200 questions et plus de 3k devrait connaître ces bases. De plus, vous voudrez peut-être acheter un nouveau clavier. La touche shift de celui-ci semble échouer par intermittence. Oh, et: il n'y a pas de "fonction de modèle" en C ++. Il existe cependant des modèles de fonctions .
sbi

Réponses:

219

Compréhension / exigences du polymorphisme

Pour comprendre le polymorphisme - comme le terme est utilisé en science informatique - il est utile de partir d'un simple test et de sa définition. Considérer:

    Type1 x;
    Type2 y;

    f(x);
    f(y);

Ici, f()consiste à effectuer une opération et reçoit des valeurs xet des yentrées.

Pour présenter un polymorphisme, il f()doit être capable de fonctionner avec des valeurs d'au moins deux types distincts (par exemple intet double), en trouvant et en exécutant un code distinct approprié au type.


Mécanismes C ++ pour le polymorphisme

Polymorphisme explicite spécifié par le programmeur

Vous pouvez écrire de f()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 intégrés, les conversions standard et la conversion / coercition sont abordés plus loin pour être complets comme suit:

  • ils sont généralement compris intuitivement de toute façon (justifiant une réaction " oh, ça "),
  • ils ont un impact sur le seuil en exigeant et la transparence dans l'utilisation des mécanismes ci-dessus, et
  • l'explication est une distraction fastidieuse des concepts plus importants.

Terminologie

Catégorisation plus poussée

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

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

    • Le temps d' exécution signifie que le compilateur doit générer du code pour tous les types que le programme peut gérer lors de l'exécution, et au moment de l'exécution, le code correct est sélectionné ( répartition virtuelle )
    • 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 fci-dessus avec des intarguments - en fonction du mécanisme polymorphe utilisé et des choix d'inlining, le compilateur peut éviter de générer du code f(double), ou le code généré peut être jeté à un moment donné de la compilation ou de la liaison. ( tous les mécanismes ci-dessus sauf l'envoi virtuel )

  • Quels types sont pris en charge?

    • 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 ceci" (selon le sens ad hoc ), un autre "ceci", et peut-être "cela" aussi ;-).
    • Cela signifie que vous pouvez simplement essayer d'utiliser la fonction pour différents types de paramètres sans rien faire spécifiquement pour activer sa prise en charge pour eux (par exemple, des modèles, des macros). Un objet avec des fonctions / opérateurs qui agissent comme le modèle / macro attend 1 est tout ce dont le modèle / macro a besoin pour faire son travail, le type exact n'étant pas pertinent. Les «concepts» introduits par C ++ 20 expriment et imposent de telles attentes - voir la page de référence ici .

      • Le polymorphisme paramétrique fournit le typage du canard - un concept attribué à James Whitcomb Riley qui a apparemment dit: «Quand je vois un oiseau qui marche comme un canard et nage comme un canard et charlatan comme un canard, j'appelle cet oiseau un canard». .

        template <typename Duck>
        void do_ducky_stuff(const Duck& x) { x.walk().swim().quack(); }
        
        do_ducky_stuff(Vilified_Cygnet());
        
    • Le polymorphisme de sous-type (aka 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 (répartition virtuelle)

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 coder que lorsque le type de données que vous traitez a un .size()membre, vous utiliserez une fonction, sinon une autre fonction qui n'en a pas besoin .size()(mais qui souffre vraisemblablement d'une certaine manière - par exemple, utiliser le plus lent strlen()ou ne pas imprimer comme utile un message 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 partielle du modèle ) ou non ( spécialisation complète ).

"Polymorphe"

Alf Steinbach commente que, dans le standard C ++, polymorphique se réfère uniquement au polymorphisme d'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 aux entités de différents types. Les fonctions virtuelles fournissent un polymorphisme dynamique (à l'exécution) via une interface fournie par une classe de base. Les fonctions et 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 ++ à Comp. Sci. terminologie.

Discussion

Avec le standard C ++ en utilisant une définition plus étroite du «polymorphisme» que le Comp. Sci. communauté, pour assurer une compréhension mutuelle de votre public, pensez à ...

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

Pourtant, ce qui est crucial pour être un grand programmeur C ++, c'est de 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 soyez donc très conscient de la façon dont les différents mécanismes polymorphes correspondent à vos besoins réels.

Le polymorphisme d'exécution convient:

  • entrée traitée par des méthodes d'usine et crachée comme une collection d'objets hétérogènes manipulé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.,
  • l'implémentation variait au moment de l'exécution, comme 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 de compilation-ce que l'on appelle des classes modèles est préférable aux interfaces lourdes échouant à l'exécution
  • SFINAE
  • CRTP
  • optimisations (beaucoup comprenant l'élimination du code inlining et mort, le déroulement de la boucle, les tableaux statiques basés sur la pile ou le tas)
  • __FILE__, __LINE__, Concaténation de chaîne littérale et d' autres capacités uniques de macros (qui restent mal ;-))
  • les modèles et les macros testent l'utilisation sémantique est prise en charge, mais ne restreignez pas artificiellement la façon dont cette prise en charge est fournie (comme la distribution virtuelle a tendance à le faire en exigeant des remplacements de fonction membre correspondant exactement)

Autres mécanismes soutenant le polymorphisme

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

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

Cette réponse se termine par une discussion sur la manière dont les éléments ci-dessus se combinent pour renforcer et simplifier le code polymorphique - en particulier le polymorphisme paramétrique (modèles et macros).

Mécanismes de mappage vers des opérations spécifiques au type

> Surcharges implicites fournies par le compilateur

Conceptuellement, le compilateur surcharge de nombreux 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 est répertorié car elle est facilement négligée. Par exemple, vous pouvez ajouter à ints et doubles en utilisant la même notation x += 2et le compilateur produit:

  • instructions 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 du polymorphisme implique généralement quelque chose de plus. (Les 2GL - langages d'assemblage - nécessitent souvent que le programmeur utilise explicitement différents 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 brouillon - espérons-le encore substantiellement correct):

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

  • Zéro ou une conversion de l'ensemble suivant: conversion de lvaleur en rvalue, conversion de tableau en pointeur et conversion de fonction en pointeur.

  • Aucune ou une conversion de l'ensemble suivant: promotions intégrales, promotion en virgule flottante, conversions intégrales, conversions en virgule flottante, conversions intégrales flottantes, conversions de pointeur, conversions de pointeur vers des membres et conversions booléennes.

  • Aucune ou une conversion de 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 autorisent du 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 intet double), en trouvant et en exécutant du code approprié au type .

a()lui-même exécute du code spécifiquement pour doubleet n'est donc pas polymorphe.

Mais, dans le second appel au a()compilateur sait générer un code de type approprié pour une « promotion de la virgule flottante » (Standard §4) pour convertir 42à 42.0. Ce code supplémentaire est dans la fonction d' appel . Nous discuterons de l'importance de cela dans la conclusion.

> Coercition, casts, 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::cinest évalué dans un contexte booléen, à l'aide d'un opérateur de conversion. Cela peut être regroupé conceptuellement avec "promotions intégrales" et al des conversions standard dans le 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*)`

Implications des surcharges, conversions et coercitions fournies 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à traiter comme un nombre réel au cours de la division (c. -à- 6,5 plutôt que vers le bas arrondi à 6), nous ne devons changement typedef double Amount.

C'est bien, mais cela n'aurait pas été trop de travail de rendre le code explicitement "type correct":

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érez 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 intou doubleet 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, des désordres verbeux et sujets aux erreurs comme:

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 d'opérateurs fournie par le compilateur pour les types intégrés, les conversions standard, le casting / la coercition / les constructeurs implicites - ils contribuent tous à un support subtil du polymorphisme. À partir de la définition en haut de cette réponse, ils traitent de «trouver et exécuter du code approprié au type» en mappant:

  • "loin" des types de paramètres

    • à partir des nombreux types de données poignées de code algorithmique polymorphes

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

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

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

Vous pouvez vous sentir trompé ... cela ne semble pas beaucoup. L'importance est que dans des contextes polymorphes paramétriques (c'est-à-dire à l'intérieur de modèles ou de macros), nous essayons de prendre en charge une gamme arbitrairement large de types, mais nous voulons souvent exprimer des opérations sur eux en termes d'autres fonctions, littéraux et opérations qui ont été conçus pour un petit ensemble de types. Cela réduit la nécessité de créer des fonctions ou des données presque identiques sur une base par type lorsque l'opération / la valeur est logiquement la même. Ces fonctionnalités coopèrent pour ajouter une attitude de «meilleur effort», faisant ce que l'on attend intuitivement en utilisant les fonctions et données disponibles limitées et ne s'arrêtant avec une erreur qu'en cas d'ambiguïté réelle.

Cela permet de limiter le besoin de code polymorphique prenant en charge le code polymorphique, de dessiner un réseau plus étroit autour de l'utilisation du polymorphisme afin que l'utilisation localisée ne force pas une utilisation généralisée, et de rendre les avantages du polymorphisme disponibles au besoin sans imposer les coûts de devoir exposer l'implémentation à au moment de la compilation, ayez 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 à l'inlining ou au moins aux appels résolus au moment de la compilation. Comme c'est généralement le cas en C ++, le programmeur dispose d'une grande liberté pour contrôler les limites dans lesquelles le polymorphisme est utilisé.

Tony Delroy
la source
1
-1 Excellente réponse sauf pour la discussion sur la terminologie. Le standard C ++ définit le terme «polymorphe» au §1.8 / 1, y faisant référence à la section 10.3 sur les fonctions virtuelles. Il n'y a donc pas de marge de manœuvre, pas de place pour la discussion, pas de place pour l'opinion personnelle: dans le contexte du C ++ standard, ce terme est défini une fois pour toutes. Et cela joue un rôle dans la pratique. Par exemple, le §5.2.7 / 6 about dynamic_castrequiert un "pointeur vers ou une lvalue d'un type polymorphe". Cheers & hth.,
Cheers and hth. - Alf
@Alf: excellente référence - même si je pense que votre perspective est trop étroite. Il est très clair d'après la question listant la surcharge, le polymorphisme ad-hoc et paramétrique, etc. que la réponse devrait relier les capacités de C ++ au Comp général. Sci. signification des termes. En effet, le glossaire de Stroustrup dit "polymorphisme - fournissant une interface unique à des entités de différents types. Les fonctions virtuelles fournissent un polymorphisme dynamique (au moment de l'exécution) via une interface fournie par une classe de base. Les fonctions et modèles surchargés fournissent un polymorphisme statique (à la compilation). TC ++ PL 12.2.6, 13.6.1, D&E 2.9. "
Tony Delroy
@Tony: ce n'est pas l'essentiel de votre réponse est faux. c'est bon, c'est super. c'est juste ça. la terminologie vous l'avez prise à l'envers: la terminologie académique formelle est la terminologie étroite définie par le Holy International Standard, et la terminologie informelle approximative où les gens peuvent signifier des choses légèrement différentes, est celle principalement utilisée dans cette question et réponse. Cheers & hth.,
Cheers and hth. - Alf le
@Alf: J'aurais aimé que la réponse soit excellente - "Autres mécanismes" doit être réécrit dans un cinquième des lignes, et je contemple / rédige des caractéristiques et implications plus concrètes contrastant avec les mécanismes polymorphes. Quoi qu'il en soit, je crois comprendre que la signification académique formelle exclusivement centrée sur C ++ peut être étroite, mais la signification générale académique formelle Comp. Sci. le sens n'est pas, comme en témoigne le glossaire de Stroustrup. Nous avons besoin de quelque chose de définitif - par exemple la définition de Knuth - pas de chance encore de googler. J'apprécie que vous soyez un gourou C ++, mais pouvez-vous indiquer des preuves pertinentes à ce sujet en particulier?
Tony Delroy
1
@Alf: deuxièmement, je suis convaincu que le polymorphisme est formellement défini dans tout Comp général décent. Sci. livre d'une manière (intemporelle, stable) compatible avec mon utilisation (et celle de Stroustrup). L'article de Wikipédia relie quelques publications académiques qui le définissent de cette façon: "Les fonctions polymorphes sont des fonctions dont les opérandes (paramètres réels) peuvent avoir plusieurs types. Les types polymorphes sont des types dont les opérations sont applicables à des valeurs de plusieurs types." (extrait de lucacardelli.name/Papers/OnUnderstanding.A4.pdf ). Alors, la question est "qui parle pour Comp. Sci" ...?
Tony Delroy
15

En C ++, la distinction importante est la liaison à l'exécution et à 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 à l'exécution peut encore être résolu au moment de la compilation, mais ce n'est qu'une optimisation. Le besoin de prendre en charge efficacement la résolution au moment de l'exécution et de faire des compromis avec d'autres problèmes fait partie de ce qui a conduit les fonctions virtuelles à ce qu'elles sont. Et c'est vraiment la clé pour toutes les formes de polymorphisme en C ++ - chacune résulte de différents ensembles de compromis effectués dans un contexte différent.

La surcharge de fonctions et la surcharge de l'opérateur sont la même chose à tous égards. 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 un autre ensemble de noms pour la même idée de temps de résolution ...

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

Ces noms sont davantage associés à la POO, il est donc un peu étrange de dire qu'un modèle ou une autre fonction non membre utilise une liaison précoce.

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

  • Premièrement, il existe 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 expédition unique. L'un des paramètres est considéré comme spécial et utilisé (avec le nom) pour identifier l'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 les paramètres contribuent à identifier l'implémentation à utiliser. Par conséquent, encore une fois, aucun des paramètres n'a besoin d'être spécial.

Il y a évidemment plus dans la POO qu'une excuse pour désigner un paramètre comme spécial, mais cela en fait partie. Et pour revenir à ce que j'ai dit à propos des compromis - l'envoi unique est assez facile à faire efficacement (l'implémentation habituelle est appelée "tables virtuelles"). La répartition multiple est plus gênante, non seulement en termes d'efficacité, mais aussi pour une compilation séparée. Si vous êtes curieux, vous pouvez rechercher "le problème de l'expression".

Tout comme il est un peu étrange d'utiliser le terme «liaison précoce» pour les fonctions non membres, il est un peu étrange d'utiliser les termes «envoi unique» et «envoi multiple» où le polymorphisme est résolu au moment de la compilation. Habituellement, C ++ est considéré comme n'ayant pas de répartition multiple, ce qui est considéré comme un type particulier de résolution d'exécution. Cependant, la surcharge de fonctions peut être considérée comme une distribution multiple effectuée au moment de la compilation.

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

Le polymorphisme paramétrique signifie que vous avez des types comme paramètres et que le même code est utilisé quel que soit le type que vous utilisez pour ces paramètres.

Le polymorphisme ad hoc est ad hoc dans le sens où vous fournissez un code différent en fonction des types particuliers.

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

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

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

Sauf que ce ne sont pas tout à fait 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 raisonnement derrière les traiter comme des synonymes est qu'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» ici peut être interprété dans le sens de la POO, mais se réfère en réalité simplement à des ensembles (généralement nommés) de types qui partagent certaines opérations.

Ainsi, le polymorphisme paramétrique est généralement pris (au moins par défaut) comme impliquant un polymorphisme sans contrainte. Étant donné que le même code est utilisé quels que soient les paramètres de type, les seules opérations prises en charge sont celles qui fonctionnent pour tous les types. En laissant l'ensemble des types sans contrainte, 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 aici est un type polymorphe sans contrainte. Cela peut être n'importe quoi, donc nous ne pouvons pas faire grand-chose avec des valeurs de ce type.

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

Ici, aest contraint d'être membre de la Numclasse - types qui agissent comme des nombres. Cette contrainte vous permet de faire des choses numérotées avec ces valeurs, comme les ajouter. Même l 3'inférence de type polymorphe indique que vous voulez dire le 3type a.

Je considère 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 contraints. L'aspect ad hoc est le choix de laquelle +et 3de l'utiliser. Chaque "instance" de Numa sa propre implémentation distincte de ceux-ci. Donc même chez Haskell "paramétrique" et "sans contrainte" ne sont pas vraiment des synonymes - ne me blâmez pas, ce n'est pas ma faute!

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

C ++ est très proche du polymorphisme paramétrique avec des modèles si chaque paramètre de modèle a un 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 substitution n'est pas une erreur» signifie que des contraintes implicites surviennent suite à l'utilisation d'opérations dans le modèle. Les complications supplémentaires incluent la spécialisation des 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 pourrait être remplacé par des alternatives ad hoc - c'est-à-dire que cette classification ne fonctionne pas vraiment pour C ++.

Steve314
la source
+1 Beaucoup de points et d'idées intéressants. Je n'ai passé que quelques heures à lire sur Haskell, donc " avoici un type polymorphe sans contrainte [...] donc nous ne pouvons pas faire grand-chose avec des valeurs de ce type". était intéressant - en C ++ sans Concepts, vous n'êtes pas limité à tenter uniquement un ensemble spécifique d'opérations sur un argument d'un type spécifié comme paramètre de modèle ... les bibliothèques comme les concepts boost fonctionnent dans l'autre sens - en vous assurant que le type prend en charge les opérations vous spécifiez, plutôt que de vous prémunir contre l'utilisation accidentelle d'opérations supplémentaires.
Tony Delroy
@Tony - Les concepts sont un moyen de contraindre explicitement le polymorphisme des modèles. Les contraintes implicites ne disparaîtront évidemment pas à cause de la compatibilité, mais les contraintes explicites amélioreront certainement les choses de manière significative. Je suis à peu près sûr que certains projets de concepts antérieurs étaient quelque peu liés aux classes de types Haskell, bien que je ne les ai pas examinées aussi profondément et que la dernière fois que j'ai regardé "superficiellement", je ne connaissais pas grand-chose à Haskell.
Steve314
"Les contraintes implicites ne disparaîtront évidemment pas à cause de la compatibilité" - de mémoire, C ++ 0x Concepts a (promis de: - /) empêcher les "contraintes implicites" - vous ne pouvez utiliser le type que de la manière promise par les Concepts.
Tony Delroy
2

Quant au polymorphisme ad hoc, cela signifie une surcharge de fonction ou une surcharge d'opérateur. 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 prises en compte car elles ne prennent pas nécessairement des 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/Parametric_polymorphism

Eric Z
la source
1
Malheureusement, bien que correct, cela est trompeur. Les fonctions de modèle peuvent obtenir des contraintes implicites à cause de la règle SFINAE - l'utilisation d'une opération dans le modèle contraint implicitement le polymorphisme - et la spécialisation de modèle peut fournir des modèles alternatifs ad hoc qui remplacent les modèles plus généraux. Ainsi, un modèle (par défaut) fournit un polymorphisme paramétrique sans contrainte, mais il n'y a pas d'application de cela - il y a au moins deux façons de devenir contraint ou ad hoc.
Steve314
En fait, votre exemple - le tri - implique une contrainte. Le tri ne fonctionne que pour les types qui sont ordonnés (c.-à-d. Fournir les <opérateurs et similaires). Dans Haskell, vous exprimeriez cette exigence explicitement en utilisant la classe Ord. Le fait que vous obteniez un différent en <fonction du type particulier (tel que fourni par l'instance de Ord) serait considéré comme un polymorphisme ad hoc.
Steve314
2

Cela n'a peut-être pas été utile, mais j'ai fait cela pour initier mes amis à la programmation en donnant des fonctions définies, comme START, et ENDpour la fonction principale, donc ce n'était pas trop intimidant (ils n'utilisaient que le fichier main.cpp ). Il contient des classes et des structures polymorphes, des modèles, des vecteurs, des tableaux, des directives de préprocesseur, une amitié, des opérateurs et des pointeurs (que vous devriez probablement savoir avant de tenter le polymorphisme):

Remarque: ce n'est pas fini, mais vous pouvez vous faire une 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;
            }
        }
    }
};
Joe
la source
1

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);
}
user2976089
la source
0

Le polymorphisme signifie de nombreuses formes en tant que tel, il est utilisé pour qu'un opérateur agisse différemment sous différentes instances. Le polymorphisme est utilisé pour implémenter l'héritage. Par exemple, nous avons défini un fn draw () pour une forme de classe, puis le draw fn peut être implémenté pour dessiner un cercle, une boîte, un triangle et d'autres formes. (qui sont des objets de la forme de classe)

Jayraj Srikriti Naidu
la source
-3

Si quelqu'un dit CUT à ces gens

The Surgeon
The Hair Stylist
The Actor

Que va-t-il se passer?

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.

Donc, la représentation ci-dessus montre ce qu'est le polymorphisme (même nom, comportement différent) en POO.

Si vous allez pour une interview et que l'intervieweur vous demande de dire / montrer un exemple en direct de polymorphisme dans la même pièce où nous sommes assis, dites-

Réponse - Porte / Fenêtres

Vous vous demandez comment?

Par porte / fenêtre - une personne peut venir, l'air peut venir, la lumière peut venir, la pluie peut venir, etc.

c'est-à-dire une forme de comportement différent (polymorphisme).

Pour mieux le comprendre et de manière simple, j'ai utilisé l'exemple ci-dessus. Si vous avez besoin de référence pour le code, suivez les réponses ci-dessus.

Sanchit
la source
Comme je l'ai mentionné pour une meilleure compréhension du polymorphisme en c ++, j'ai utilisé l'exemple ci-dessus. Cela pourrait aider un plus novice à comprendre et à comprendre ce que signifie ou ce qui se passe derrière le code lors de l'entretien. Je vous remercie!
Sanchit
op a demandé "polymorphisme en c ++". votre réponse est bien trop abstraite.
StahlRat