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 x
et des y
entrées.
Pour présenter un polymorphisme, il f()
doit être capable de fonctionner avec des valeurs d'au moins deux types distincts (par exemple int
et 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:
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 à int
s et double
s en utilisant la même notation x += 2
et 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 int
et double
), en trouvant et en exécutant du code approprié au type .
a()
lui-même exécute du code spécifiquement pour double
et 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::cin
est é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 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, 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:
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é.