Chargement...

Polymorphisme en C++ : fonctions virtuelles et vtable

Apprenez le polymorphisme en C++ avec les fonctions virtuelles, la vtable et la liaison dynamique à travers des exemples.

Le polymorphisme permet à des objets partageant la même interface (type de base) d’adopter des comportements différents à l’exécution. En C++, le polymorphisme dynamique est réalisé grâce aux fonctions virtuelles, et le compilateur implémente ce mécanisme à l’aide d’une vtable (table virtuelle) et d’un vptr (pointeur virtuel) présent dans chaque objet. Dans cet article, nous verrons le fonctionnement des fonctions virtuelles, la structure vtable/vptr, les implications sur les performances et les meilleures pratiques, à travers des exemples concrets.


1) Qu’est-ce que le polymorphisme dynamique ?

Le polymorphisme dynamique signifie que la fonction appelée est déterminée à l’exécution. Cela permet d’appeler le comportement d’une classe dérivée à travers un pointeur ou une référence de la classe de base.


#include <iostream>
#include <memory>
using namespace std;

struct Book {
    virtual ~Book() = default;           // nécessaire pour la base polymorphe
    virtual void PrintInfo() const {     // fonction virtuelle
        cout << "[Book] Informations générales" << endl;
    }
};

struct EBook : Book {
    void PrintInfo() const override {
        cout << "[EBook] Édition numérique" << endl;
    }
};

struct PrintedBook : Book {
    void PrintInfo() const override {
        cout << "[PrintedBook] Édition papier" << endl;
    }
};

int main() {
    unique_ptr<Book> a = make_unique<EBook>();
    unique_ptr<Book> b = make_unique<PrintedBook>();

    a->PrintInfo();  // [EBook] ...
    b->PrintInfo();  // [PrintedBook] ...
}

Bien que a et b soient du type de base (Book), les appels sont résolus à l’exécution selon le type réel (EBook/PrintedBook).


2) Comment fonctionnent vtable et vptr ?

Lorsqu’une classe contient au moins une fonction virtual, le compilateur génère une vtable pour cette classe. Chaque objet contient alors un champ caché vptr qui pointe vers la vtable correspondante.

Lors d’un appel : objet.vptr → vtable → adresse réelle de la fonction.


Book (vptr) ──► vtable(Book)       : &Book::PrintInfo
EBook (vptr) ─► vtable(EBook)       : &EBook::PrintInfo
Printed (vptr)► vtable(PrintedBook) : &PrintedBook::PrintInfo

Cette redirection indirecte ajoute un léger coût d’appel, généralement négligeable.


3) override, final et destructeur virtuel

Le mot-clé override vérifie à la compilation qu’une fonction virtuelle de la base est bien redéfinie. final empêche toute redéfinition ultérieure. Dans une classe de base polymorphe, un destructeur virtuel est essentiel : sans lui, la suppression via un pointeur de base entraîne un comportement indéfini.


struct Book {
    virtual ~Book() = default;
    virtual double Price() const { return 0.0; }
};

struct EBook final : Book {
    double Price() const override { return 99.0; }
};

// struct SpecialEBook : EBook {}; // ERREUR : EBook est final

4) Différence avec le polymorphisme statique (surcharge/templates)

Les surcharges (même nom, signature différente) et les templates sont résolus à la compilation, sans utiliser de vtable. Le polymorphisme dynamique, lui, est résolu à l’exécution via l’appel virtuel.


5) Conception d’une interface polymorphe (exemple : Book)

Factorisez le comportement commun dans la classe de base et redéfinissez-le dans les classes dérivées. Exemple de calcul de prix pour les livres numériques et imprimés.


#include <iostream>
#include <memory>
#include <string>
using namespace std;

class Book {
protected:
    string title, author;
public:
    Book(string t, string a) : title(t), author(a) {}
    virtual ~Book() = default;

    virtual double FinalPrice() const = 0;   // pure virtual : contrat abstrait
    virtual void PrintInfo() const {
        cout << title << " - " << author
             << " | " << FinalPrice() << " €" << endl;
    }
};

class EBook : public Book {
    double base, vat;
public:
    EBook(string t, string a, double b, double v)
        : Book(t,a), base(b), vat(v) {}

    double FinalPrice() const override {
        return base*(1.0+vat);
    }
};

class PrintedBook : public Book {
    double base, ship; int pages; bool hard;
public:
    PrintedBook(string t, string a, double b, double s, int p, bool h)
        : Book(t,a), base(b), ship(s), pages(p), hard(h) {}

    double FinalPrice() const override {
        double cost = base + ship + 0.04*pages;
        if (hard) cost *= 1.1;
        return cost;
    }
};

int main() {
    unique_ptr<Book> b1 = make_unique<EBook>("Modern C++", "A. Dev", 100, 0.10);
    unique_ptr<Book> b2 = make_unique<PrintedBook>("Clean Code", "R. Martin", 120, 20, 464, true);

    b1->PrintInfo(); // comportement EBook
    b2->PrintInfo(); // comportement PrintedBook
}

6) Object Slicing et utilisation de références/pointeurs

Copier un objet dérivé par valeur dans un type de base « tranche » la partie spécifique du dérivé. Pour le polymorphisme, utilisez toujours une référence ou un pointeur de base.


PrintedBook pb("X","Y",100,20,200,false);
Book base = pb;   // SLICING : seule la partie Book est copiée
base.PrintInfo(); // comportement de Book (partie dérivée perdue)

7) dynamic_cast et sécurité des types

Utilisez dynamic_cast pour des conversions sûres d’un type de base vers un type dérivé (RTTI nécessaire). En cas d’échec, retourne nullptr (pour les pointeurs).


void PrintIfPrinted(const Book* b) {
    if (auto pb = dynamic_cast<const PrintedBook*>(b)) {
        cout << "Prix imprimé : " << pb->FinalPrice() << endl;
    } else {
        cout << "Pas un PrintedBook." << endl;
    }
}

8) Remarques sur les performances


9) Classe/méthode final, erreurs override et bonnes pratiques


10) Visualisation du fonctionnement de la vtable (étape par étape)

  1. La classe contient au moins une fonction virtuelle → le compilateur crée une vtable.
  2. Lors de la création d’un objet, le vptr pointe vers la vtable de sa classe.
  3. Lors d’un appel, le compilateur obtient l’adresse via vptr → vtable au lieu d’un lien statique.
  4. Une redéfinition (override) remplace l’entrée correspondante dans la vtable de la classe dérivée.

11) Erreurs fréquentes


12) Exemple complet : collection polymorphe


#include <iostream>
#include <vector>
#include <memory>
using namespace std;

class Book {
public:
    virtual ~Book() = default;
    virtual double FinalPrice() const = 0;
    virtual void Print() const = 0;
};

class EBook : public Book {
    double base, vat;
public:
    EBook(double b, double v) : base(b), vat(v) {}
    double FinalPrice() const override { return base*(1+vat); }
    void Print() const override {
        cout << "[EBook] " << FinalPrice() << " €" << endl;
    }
};

class PrintedBook : public Book {
    double base, ship; int pages;
public:
    PrintedBook(double b, double s, int p) : base(b), ship(s), pages(p) {}
    double FinalPrice() const override { return base + ship + 0.05*pages; }
    void Print() const override {
        cout << "[Printed] " << FinalPrice() << " €" << endl;
    }
};

int main() {
    vector<unique_ptr<Book>> items;
    items.push_back(make_unique<EBook>(100, 0.1));
    items.push_back(make_unique<PrintedBook>(120, 20, 400));

    for (const auto& it : items) it->Print(); // polymorphisme dynamique
}

13) TL;DR

  • Le polymorphisme dynamique en C++ repose sur les fonctions virtuelles.
  • Le compilateur les implémente avec une vtable/vptr.
  • Un destructeur virtuel est indispensable pour les bases polymorphes.
  • override garantit la bonne redéfinition ; final empêche de futures modifications.
  • Utilisez références/pointeurs pour éviter le slicing.
  • Le surcoût est généralement faible ; pour les boucles critiques, envisagez CRTP ou des surcharges.
  • Tous les exemples fonctionnent sous Visual Studio 2022 et GCC 11+.

Articles connexes