Maîtriser le Polymorphisme, la Surcharge d'Opérateurs et les Templates en C++

Polymorphisme d'Exécution et Classes Abstraites

Le polymorphisme d'exécution en C++ repose sur l'utilisation de fonctions virtuelles et de pointeurs ou références vers une classe de base. Lorsqu'une classe contient au moins une fonction virtuelle pure, elle devient une classe abstraite. Une classe abstraite définit un contrat (une interface) et ne peut pas être instanciée directement. Toute classe dérivée doit implémenter ces fonctions pures, sous peine de devenir elle-même abstraite.

Un point crucial lors de l'utilisation du polymorphisme est la gestion de la mémoire. Si un objet dérivé est supprimé via un poniteur de base, le destructeur de la classe de base doit être déclaré virtual. Sans cela, le destructeur de la classe dérivée ne sera pas appelé, entraînant des fuites de mémoire. L'utilisation de pointeurs intelligents comme std::unique_ptr est fortement recommandée pour automatiser cette gestion.

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// Classe abstraite définissant l'interface
class DigitalAsset {
protected:
    std::string title;

public:
    explicit DigitalAsset(std::string t) : title(std::move(t)) {}
    virtual ~DigitalAsset() = default; // Destructeur virtuel essentiel

    virtual void render() const = 0;
    virtual void consume() const = 0;
};

class Document : public DigitalAsset {
    std::string writer;
public:
    Document(std::string t, std::string w) : DigitalAsset(std::move(t)), writer(std::move(w)) {}
    
    void render() const override { 
        std::cout << "Affichage du document '" << title << "' par " << writer << '\n'; 
    }
    void consume() const override { 
        std::cout << "Lecture du document en cours...\n"; 
    }
};

class VideoClip : public DigitalAsset {
    std::string director;
public:
    VideoClip(std::string t, std::string d) : DigitalAsset(std::move(t)), director(std::move(d)) {}
    
    void render() const override { 
        std::cout << "Lecture de la vidéo '" << title << "' réalisée par " << director << '\n'; 
    }
    void consume() const override { 
        std::cout << "Visionnage de la vidéo...\n"; 
    }
};

void processAssets() {
    std::vector<std::unique_ptr<DigitalAsset>> assets;
    
    assets.push_back(std::make_unique<Document>("Architecture Logicielle", "J. Ousterhout"));
    assets.push_back(std::make_unique<VideoClip>("Inception", "C. Nolan"));

    for (const auto& asset : assets) {
        asset->render();
        asset->consume();
        std::cout << '\n';
    }
}

int main() {
    processAssets();
    return 0;
}

Note techniqeu : Lors de la redéfinition des méthodes dans les classes dérivées, la signature doit correspondre exactement à celle de la classe de base, y compris les qualificateurs comme const. L'omission de const dans la méthode dérivée masquera la méthode de base au lieu de la redéfinir, ce qui provoquera une erreur de compilation si le mot-clé override est utilisé.

Surcharge d'Opérateurs et Algorithmes de la STL

La surcharge d'opérateurs permet d'intégrer les classes personnalisées de manière transparente avec les flux d'entrée/sortie standard et les algorithmes de la Standard Template Library (STL). En surchargeant l'opérateur <<, on facilite l'affichage des objets. Combiné à des comparateurs personnalisés, cela rend le tri de collections d'objets très expressif.

#include <iostream>
#include <vector>
#include <algorithm>
#include <iomanip>
#include <string>

class HardwareComponent {
    std::string modelName;
    double baseCost;

public:
    HardwareComponent(std::string name, double cost) 
        : modelName(std::move(name)), baseCost(cost) {}

    friend std::ostream& operator<<(std::ostream& os, const HardwareComponent& comp) {
        return os << "Modèle: " << comp.modelName << " | Coût: " << comp.baseCost;
    }
};

class SalesTransaction {
    HardwareComponent component;
    int unitsSold;
    double retailPrice;

public:
    SalesTransaction(HardwareComponent comp, int units, double price) 
        : component(std::move(comp)), unitsSold(units), retailPrice(price) {}

    int getUnits() const { return unitsSold; }
    double computeRevenue() const { return unitsSold * retailPrice; }

    friend std::ostream& operator<<(std::ostream& os, const SalesTransaction& tx) {
        return os << tx.component << " | Unités: " << tx.unitsSold 
                  << " | Revenu: " << tx.computeRevenue();
    }
};

// Comparateur pour un tri décroissant basé sur le nombre d'unités vendues
bool compareByUnitsDesc(const SalesTransaction& a, const SalesTransaction& b) {
    return a.getUnits() > b.getUnits();
}

void analyzeSales() {
    std::vector<SalesTransaction> ledger;
    
    ledger.emplace_back(HardwareComponent("GPU RTX", 800.0), 15, 1200.0);
    ledger.emplace_back(HardwareComponent("CPU Ryzen", 300.0), 45, 450.0);
    ledger.emplace_back(HardwareComponent("SSD NVMe", 50.0), 120, 90.0);

    // Tri décroissant
    std::sort(ledger.begin(), ledger.end(), compareByUnitsDesc);

    std::cout << "=== Rapport des Ventes (Trié par Volume) ===\n";
    for (const auto& record : ledger) {
        std::cout << record << '\n';
    }
}

int main() {
    analyzeSales();
    return 0;
}

Dans cet exemple, l'opérateur << est surchargé à deux niveaux : d'abord pour HardwareComponent, puis pour SalesTransaction qui réutilise la surcharge interne. La fonction std::sort utilise le prédicat compareByUnitsDesc pour organiser les transactions de la plus volumineuse à la plus faible.

Templates de Classes et Programmation Générique

Les templates (gabarits) permettent d'écrire du code agnostique au type de données. Une classe template est instanciée par le compilateur pour chaque type utilisé. Lors de la surcharge d'opérateurs pour des classes templates, il est courant d'utiliser des fonctions amies (friend) pour accéder aux membres privés tout en permettant la conversion implicite des opérandes.

#include <iostream>
#include <cmath>

template <typename Numeric>
class ComplexNumber {
private:
    Numeric re;
    Numeric im;

public:
    ComplexNumber(Numeric realPart = Numeric(), Numeric imagPart = Numeric()) 
        : re(realPart), im(imagPart) {}

    Numeric getReal() const { return re; }
    Numeric getImag() const { return im; }

    ComplexNumber& operator+=(const ComplexNumber& other) {
        re += other.re;
        im += other.im;
        return *this;
    }

    friend std::ostream& operator<<(std::ostream& os, const ComplexNumber& num) {
        os << num.re << (num.im >= 0 ? " + " : " - ") << std::abs(num.im) << "i";
        return os;
    }

    friend std::istream& operator>>(std::istream& is, ComplexNumber& num) {
        is >> num.re >> num.im;
        return is;
    }

    friend ComplexNumber operator+(const ComplexNumber& lhs, const ComplexNumber& rhs) {
        ComplexNumber result(lhs);
        result += rhs;
        return result;
    }

    friend bool operator==(const ComplexNumber& lhs, const ComplexNumber& rhs) {
        return (lhs.re == rhs.re) && (lhs.im == rhs.im);
    }
};

void testComplexMath() {
    ComplexNumber<double> z1(3.5, -2.0);
    ComplexNumber<double> z2(1.5, 4.0);
    
    std::cout << "z1 = " << z1 << '\n';
    std::cout << "z2 = " << z2 << '\n';
    std::cout << "z1 + z2 = " << (z1 + z2) << '\n';
    
    z1 += z2;
    std::cout << "z1 après addition = " << z1 << '\n';
    
    std::cout << std::boolalpha << "z1 == z2 ? " << (z1 == z2) << '\n';
}

int main() {
    testComplexMath();
    return 0;
}

L'implémentation de ComplexNumber illustre l'utilisation de paramètres par défaut dans le constructeur et la délégation de l'opérateur binaire + à l'opérateur composé +=. L'utilisation de std::abs dans la surcharge de l'opérateur de flux garantit un affichage mathématiquement correct, évitant des résultats comme 3 + -2i.

Étiquettes: C++ polymorphisme Templates C++ Surcharge d'opérateurs pointeurs intelligents

Publié le 27 août à 14h50