Polymorphisme : Compilation vs Exécution
Fait partie de la section Programmation Orientée Objet du Journey C++ de Coddy — leçon 56 sur 104.
Le polymorphisme signifie « plusieurs formes » et constitue un concept fondamental de la POO qui permet de traiter les objets de manière uniforme tout en ayant des comportements différents. Le C++ prend en charge deux types distincts de polymorphisme, chacun étant résolu à une étape différente de l'exécution du programme.
Le polymorphisme à la compilation (également appelé polymorphisme statique) est résolu par le compilateur avant l'exécution du programme. Le compilateur détermine exactement quelle fonction appeler en se basant sur la signature de la fonction. Cela inclut la surcharge de fonctions et les templates :
void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
print(5); // Le compilateur choisit print(int)
print(3.14); // Le compilateur choisit print(double)Le polymorphisme d'exécution (également appelé polymorphisme dynamique) est résolu pendant l'exécution du programme. La décision concernant la fonction à appeler dépend du type réel de l'objet, et non du type de pointeur ou de référence. Ceci est réalisé grâce aux fonctions virtuelles :
class Shape {
public:
virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing circle" << std::endl; }
};
Shape* s = new Circle();
s->draw(); // Décidé à l'exécution : "Drawing circle"Le compromis clé : le polymorphisme à la compilation n'a aucun surcoût à l'exécution puisque les décisions sont prises lors de la compilation, tandis que le polymorphisme à l'exécution ajoute un léger coût (recherche dans la vtable) mais offre une plus grande flexibilité pour travailler avec des objets dont les types ne sont pas connus avant l'exécution.
Défi
FacileConstruisons un système de calculatrice qui démontre les deux types de polymorphisme côte à côte. Vous allez créer un système où le polymorphisme à la compilation gère différents types d'entrées via la surcharge de fonctions, tandis que le polymorphisme à l'exécution permet d'échanger dynamiquement différentes stratégies de calcul.
Vous organiserez votre code sur trois fichiers :
Calculator.h: Définissez une classe de baseCalculatorqui représente n'importe quelle stratégie de calcul :- Une méthode virtuelle
calculate(int a, int b)qui retourne unintet affiche :Base calculation: <a> ? <b>(en retournant 0) - Un destructeur virtuel
- Une méthode virtuelle
Operations.h: Définissez deux classes de calculatrices dérivées qui surchargent le comportement de calcul :Adder: Surchargezcalculate()pour afficherAdding: <a> + <b>et retourner la sommeMultiplier: Surchargezcalculate()pour afficherMultiplying: <a> * <b>et retourner le produit
override.main.cpp: Créez un système qui présente les deux types de polymorphisme. Lisez deux entrées entières (chacune sur une ligne séparée).Tout d'abord, démontrez le polymorphisme à la compilation en créant trois fonctions
display()surchargées :display(int x)affiche :Integer value: <x>display(double x)affiche :Double value: <x>display(const std::string& x)affiche :String value: <x>
Ensuite, démontrez le polymorphisme à l'exécution en créant un tableau de pointeurs
Calculator*contenant une baseCalculator, unAdderet unMultiplier. Parcourez le tableau et appelezcalculate()sur chacun avec vos valeurs d'entrée, en affichant le résultat après chaque calcul.Structurez votre sortie comme suit :
=== Compile-Time Polymorphism === <sorties display pour int, double, string> === Runtime Polymorphism === <sorties calculate avec résultats>Pour la section à la compilation, appelez
display()avec la première entrée en tant qu'entier, puis en tant que double (même valeur avec .5 ajouté), puis en tant que chaîne "Result". Libérez vos calculatrices allouées dynamiquement une fois terminé.
Par exemple, avec les entrées 10 et 3 :
=== Compile-Time Polymorphism ===
Integer value: 10
Double value: 10.5
String value: Result
=== Runtime Polymorphism ===
Base calculation: 10 ? 3
Result: 0
Adding: 10 + 3
Result: 13
Multiplying: 10 * 3
Result: 30Remarquez comment le compilateur sélectionne la bonne surcharge de display() en fonction du type d'argument (décision à la compilation), tandis que la bonne méthode calculate() est déterminée par le type réel de l'objet à l'exécution via le mécanisme de vtable.
Aide-mémoire
Le C++ prend en charge deux types de polymorphisme résolus à différentes étapes :
Le polymorphisme à la compilation (polymorphisme statique) est résolu par le compilateur avant l'exécution. Il comprend la surcharge de fonctions et les templates :
void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
print(5); // Le compilateur choisit print(int)
print(3.14); // Le compilateur choisit print(double)Le polymorphisme à l'exécution (polymorphisme dynamique) est résolu pendant l'exécution à l'aide de fonctions virtuelles. Le type réel de l'objet détermine quelle fonction est appelée :
class Shape {
public:
virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing circle" << std::endl; }
};
Shape* s = new Circle();
s->draw(); // Décidé à l'exécution : "Drawing circle"Compromis : Le polymorphisme à la compilation n'a aucun surcoût à l'exécution, tandis que le polymorphisme à l'exécution ajoute un léger coût (recherche dans la vtable) mais offre une plus grande flexibilité.
Essayez vous-même
#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"
// TODO: Créer trois fonctions display() surchargées :
// 1. display(int x) - affiche "Integer value: <x>"
// 2. display(double x) - affiche "Double value: <x>"
// 3. display(const std::string& x) - affiche "String value: <x>"
int main() {
// Lire deux entrées entières
int a, b;
std::cin >> a;
std::cin >> b;
// === Compile-Time Polymorphism ===
std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
// TODO: Appeler display() avec :
// - a en tant qu'entier
// - a en tant que double (lui ajouter 0.5)
// - la chaîne "Result"
std::cout << std::endl;
// === Runtime Polymorphism ===
std::cout << "=== Runtime Polymorphism ===" << std::endl;
// TODO: Créer un tableau de pointeurs Calculator* avec 3 éléments :
// - un Calculator de base
// - un Adder
// - un Multiplier
// TODO: Parcourir le tableau, appeler calculate(a, b) sur chaque élément,
// et afficher "Result: <return_value>" après chaque calcul
// TODO: Libérer la mémoire allouée dynamiquement
return 0;
}
Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.
Toutes les leçons de Programmation Orientée Objet
1Fondamentaux de la POO
Fichiers externesBuild et Compilation C++Fichiers d'en-tête et Fichiers sourceEspaces de noms et PortéeIntroduction à la POO en C++Classes vs ObjetsLe pointeur 'this'Méthodes (Fonctions membres)Attributs (Membres de données)Bases des Ctors et DtorsRécapitulatif - Calculatrice simple4Propriétés de classe
Membres d'instance vs membres statiquesGetters et SettersFonctions membres constMot-clé mutableMéthodes et variables statiquesFonctions et classes amiesRécapitulatif - Gestionnaire de compte bancaire7Héritage
Héritage de baseNiveaux d'accès à l'héritageOrdre d'appel des Ctor et DtorRedéfinition de méthodesFonctions virtuelles et VTableHéritage multipleHéritage virtuelRécapitulatif - Hiérarchie des employés10Présentation de la STL
Présentation et philosophie de la STLConteneurs de la STLItérateursAlgorithmes de la STLFoncteurs et expressions lambdaRécapitulatif - Fréquence des mots13Patrons de conception, Partie 1
Introduction aux patrons de conceptionPatron SingletonFabrique & Fabrique abstraitePatron MonteurPatron ObservateurPatron Stratégie2Gestion de la mémoire
Mémoire Pile vs TasPointeurs et RéférencesMémoire dynamique (new/delete)Smart Pointers en C++RAII en C++Récapitulatif - Gestionnaire de tableaux dynamiques5Encapsulation
Spécificateurs d'accès en C++Spécificateurs d'accès en profondeurMasquage d'informationStruct vs ClassClasses imbriquées et internesRécapitulatif - Système de gestion d'étudiants8Polymorphisme
Polymorphisme : Compilation vs ExécutionSurcharge de fonctionsRetour sur les fonctions virtuellesFonctions virtuelles puresClasses abstraitesConception d'interfaces en C++Dynamic Casting & RTTIRécapitulatif - Calculateur de formes11Concepts avancés de la POO
Composition vs HéritageMixins via CRTPIdiome PimplEffacement de typeEnum Classes & Typage fortGestion des exceptions en POOHiérarchies d'exceptions personnalisées14Patrons de conception, Partie 2
Patron CommandePatron AdaptateurPatron DécorateurPatron Template MethodPatron ÉtatPatron CompositeRAII en tant que patron3Constructeurs et Destructeurs
Constructeur par défautConstructeur paramétréConstructeur de copieConstructeur de déplacementListes d'initialisation du constructeurConstructeurs déléguésAnalyse approfondie du destructeurRègle de trois / cinq / zéroRécapitulatif - Classe String6Surcharge d'opérateurs
Introduction à la surcharge d'opérateursSurcharge des opérateurs arithmétiquesSurcharge des opérateurs de comparaisonOpérateurs de fluxSurcharge de l'opérateur d'affectationSurcharge des opérateurs [] et ()Opérateurs de conversion de typeRécapitulatif - Classe Matrix9Templates
Templates de fonctionsTemplates de classesSpécialisation de templatesTemplates variadiquesBases de SFINAE & Type TraitsRécapitulatif - Conteneur générique