Chargement...

Classes abstraites et interfaces en C++

Apprenez les classes abstraites et les interfaces en C++ avec les fonctions virtuelles pures et des exemples pratiques.

En C++, les classes abstraites sont des classes contenant au moins une fonction purement virtuelle et qui ne peuvent pas être instanciées directement. Le langage C++ ne possède pas de mot-clé distinct pour « interface » ; à la place, une classe abstraite composée uniquement de fonctions virtuelles pures joue le rôle d’interface. Dans cet article, nous aborderons la conception des classes abstraites et des interfaces, la mise en œuvre de multiples interfaces, le principe d’inversion de dépendance (DIP) et un scénario pratique avec un exemple de Book.


1) Fonction virtuelle pure et classe abstraite

Une fonction virtuelle marquée avec = 0 est une fonction purement virtuelle. Une classe contenant une telle fonction devient abstraite et ne peut pas être instanciée directement.


struct IPrintable {
    virtual ~IPrintable() = default;    // Bonnes pratiques : destructeur virtuel dans les interfaces
    virtual void Print() const = 0;     // pure virtuelle → contrat d’interface
};

struct IEntity {
    virtual ~IEntity() = default;
    virtual std::string Key() const = 0;
};

Ces deux classes jouent désormais le rôle « d’interfaces ». Les classes qui les implémentent doivent définir Print() et Key().


2) Classe de base Book : classe abstraite

Exprimons le contrat commun à l’aide d’une classe abstraite BookBase. Les classes dérivées définiront la logique de calcul du prix.


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

struct BookBase : IPrintable, IEntity {
protected:
    string title, author;
public:
    BookBase(string t, string a) : title(move(t)), author(move(a)) {}
    virtual ~BookBase() = default;

    // Pure virtuelle : les classes dérivées calculent le prix
    virtual double FinalPrice() const = 0;

    // Contrat IPrintable
    void Print() const override {
        cout << title << " - " << author
             << " | " << FinalPrice() << " €" << endl;
    }

    // Contrat IEntity
    string Key() const override { return title + ":" + author; }
};

BookBase ne peut pas être instanciée directement avec new car FinalPrice() est purement virtuelle. Cependant, nous pouvons fournir des implémentations par défaut pour Print() et Key().


3) Classes implémentant l’interface (EBook / PrintedBook)

Créons deux types de livres différents qui respectent le contrat FinalPrice().


struct EBook : BookBase {
    double base, vat;
    EBook(string t, string a, double b, double v)
        : BookBase(move(t), move(a)), base(b), vat(v) {}

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

enum class Cover { Soft, Hard };

struct PrintedBook : BookBase {
    double base, shipping; int pages; Cover cover;
    PrintedBook(string t, string a, double b, double s, int p, Cover c)
        : BookBase(move(t), move(a)), base(b), shipping(s), pages(p), cover(c) {}

    double FinalPrice() const override {
        double cost = base + shipping + 0.04 * pages;
        if (cover == Cover::Hard) cost *= 1.1;
        return cost;
    }
};

Les deux classes respectent le contrat BookBase et peuvent donc être utilisées polymorphiquement.


4) Implémentation multiple d’interfaces (Segregation d’interface)

Concevoir de petites interfaces ciblées plutôt qu’une seule grande interface réduit les coûts de maintenance. Par exemple, une interface IDiscountable peut séparer la logique de remise.


struct IDiscountable {
    virtual ~IDiscountable() = default;
    virtual double ApplyDiscount(double rate) const = 0; // 0.10 → 10 %
};

struct DiscountedEBook : EBook, IDiscountable {
    using EBook::EBook; // hérite des constructeurs
    double ApplyDiscount(double rate) const override {
        return FinalPrice() * (1.0 - rate);
    }
};

Avec ce design, DiscountedEBook implémente à la fois le contrat BookBase et l’interface de remise.


5) Inversion de dépendance (DIP) et testabilité

Les classes doivent dépendre des abstractions (interfaces) et non des types concrets. Cela favorise un couplage faible et facilite les tests unitaires. Ci-dessous, un « moteur de tarification » utilise uniquement l’interface BookBase.


struct PricingEngine {
    double Total(const vector<shared_ptr<BookBase>>& items) const {
        double sum = 0.0;
        for (const auto& b : items) sum += b->FinalPrice();
        return sum;
    }
};

int main() {
    vector<shared_ptr<BookBase>> items;
    items.push_back(make_shared<EBook>("Modern C++", "A. Dev", 100, 0.1));
    items.push_back(make_shared<PrintedBook>("Clean Code", "R. Martin", 120, 20, 464, Cover::Hard));

    PricingEngine eng;
    cout << "Total : " << eng.Total(items) << " €" << endl;

    for (const auto& b : items) b->Print(); // IPrintable
}

PricingEngine ne dépend pas de classes concrètes ; elle travaille uniquement avec BookBase. Vous pouvez ainsi injecter un MockBook pour les tests unitaires.


6) Interfaces et héritage multiple

C++ prend en charge l’héritage multiple. Pour les interfaces, cela est généralement sûr car elles ne contiennent pas d’état. L’héritage multiple de classes avec état peut toutefois provoquer le problème du diamant. Dans ce cas, utilisez l’héritage virtuel ou de préférence la composition.


struct IStorable  { virtual ~IStorable()=default;  virtual void Save() const =0; };
struct ISearchable{ virtual ~ISearchable()=default; virtual bool Matches(string) const =0; };

struct SearchableBook : BookBase, IStorable, ISearchable {
    using BookBase::BookBase;

    double FinalPrice() const override { return 0.0; } // exemple simple
    void Save() const override { /* écrire dans un fichier... */ }
    bool Matches(string q) const override {
        return title.find(q)!=string::npos || author.find(q)!=string::npos;
    }
};

7) Exemple de fabrique (Factory) avec interface

Pour séparer la logique de création des classes concrètes, nous pouvons utiliser une interface de fabrique.


struct IBookFactory {
    virtual ~IBookFactory() = default;
    virtual shared_ptr<BookBase> MakeEBook(string t,string a,double b,double v)=0;
    virtual shared_ptr<BookBase> MakePrinted(string t,string a,double b,double s,int p,Cover c)=0;
};

struct DefaultBookFactory : IBookFactory {
    shared_ptr<BookBase> MakeEBook(string t,string a,double b,double v) override {
        return make_shared<EBook>(move(t), move(a), b, v);
    }
    shared_ptr<BookBase> MakePrinted(string t,string a,double b,double s,int p,Cover c) override {
        return make_shared<PrintedBook>(move(t), move(a), b, s, p, c);
    }
};

Les modules de haut niveau dépendent uniquement de IBookFactory ; la création concrète peut être facilement remplacée ou simulée dans les tests.


8) Conseils de conception


9) Erreurs courantes


10) TL;DR

  • En C++, une « interface » = une classe abstraite composée uniquement de fonctions virtuelles pures.
  • Les classes abstraites ne peuvent pas être instanciées ; les classes dérivées deviennent concrètes en redéfinissant le contrat.
  • Préférez des interfaces petites et cohérentes (ISP) et dépendez des abstractions (DIP).
  • Utilisez des pointeurs/références pour le polymorphisme ; n’oubliez pas le destructeur virtuel.
  • Utilisez l’héritage multiple pour les interfaces ; pour le partage d’état, préférez la composition.
  • Tous les exemples fonctionnent sous Visual Studio 2022 et GCC 11+.

Articles connexes