🚀 Nuova versione beta disponibile! Feedback o problemi? Contattaci

Polimorfismo in C++

Codegrind TeamAug 23 2024

Il polimorfismo è uno dei pilastri della programmazione orientata agli oggetti (OOP) e rappresenta la capacità di utilizzare una singola interfaccia per oggetti di diversi tipi. In C++, il polimorfismo consente di trattare oggetti di diverse classi derivate attraverso un puntatore o riferimento di una classe base, permettendo una maggiore flessibilità ed estensibilità del codice. Questo articolo esplorerà il concetto di polimorfismo in C++, i tipi principali di polimorfismo e come implementarlo efficacemente nel tuo codice.

Tipi di Polimorfismo

Esistono principalmente due tipi di polimorfismo in C++:

  1. Polimorfismo di Sovraccarico (Ad-hoc Polymorphism): Si riferisce alla capacità di un singolo nome di funzione o operatore di comportarsi in modi diversi in base ai tipi degli argomenti. Comprende l’overload delle funzioni e l’overload degli operatori.
  2. Polimorfismo di Sottotipo (Subtyping Polymorphism) o Polimorfismo di Inclusione: Si verifica quando una funzione tratta oggetti di diverse classi derivate come oggetti di una classe base comune. È implementato principalmente attraverso l’uso di funzioni virtuali.

Esempio di Polimorfismo di Sovraccarico

Il polimorfismo di sovraccarico è già stato trattato in articoli precedenti attraverso l’overload delle funzioni e degli operatori.

Esempio di Polimorfismo di Sottotipo

Il polimorfismo di sottotipo consente a un puntatore o riferimento di una classe base di invocare metodi di una classe derivata.

Implementazione del Polimorfismo tramite Funzioni Virtuali

Per implementare il polimorfismo di sottotipo in C++, utilizziamo funzioni virtuali. Una funzione virtuale è una funzione membro di una classe base che può essere sovrascritta in una classe derivata. Quando una funzione virtuale viene chiamata su un puntatore o riferimento a una classe base, C++ invoca la versione della funzione definita nella classe derivata più specifica.

Esempio di Classe Base e Derivate

Supponiamo di avere una classe base Forma e due classi derivate Cerchio e Rettangolo.

#include <iostream>
#include <cmath>

class Forma {
public:
    virtual void disegna() const {
        std::cout << "Disegna una forma generica." << std::endl;
    }

    virtual double area() const = 0;  // Funzione virtuale pura
};

class Cerchio : public Forma {
private:
    double raggio;
public:
    Cerchio(double r) : raggio(r) {}

    void disegna() const override {
        std::cout << "Disegna un cerchio." << std::endl;
    }

    double area() const override {
        return M_PI * raggio * raggio;
    }
};

class Rettangolo : public Forma {
private:
    double larghezza, altezza;
public:
    Rettangolo(double l, double h) : larghezza(l), altezza(h) {}

    void disegna() const override {
        std::cout << "Disegna un rettangolo." << std::endl;
    }

    double area() const override {
        return larghezza * altezza;
    }
};

Uso del Polimorfismo

Il polimorfismo permette di trattare oggetti di classi diverse, ma con un’interfaccia comune, in modo uniforme.

int main() {
    Forma* forme[2];
    forme[0] = new Cerchio(5.0);
    forme[1] = new Rettangolo(4.0, 6.0);

    for (int i = 0; i < 2; ++i) {
        forme[i]->disegna();
        std::cout << "Area: " << forme[i]->area() << std::endl;
    }

    // Deallocazione della memoria
    delete forme[0];
    delete forme[1];

    return 0;
}

Uscita

Disegna un cerchio.
Area: 78.5398
Disegna un rettangolo.
Area: 24

In questo esempio, anche se il tipo di forme[i] è Forma*, il metodo corretto (disegna e area) per ciascuna classe derivata viene chiamato grazie al polimorfismo.

Funzioni Virtuali Pure e Classi Astratte

In C++, una funzione virtuale pura è una funzione che deve essere sovrascritta in ogni classe derivata. Viene dichiarata nella classe base con = 0, rendendo la classe base una classe astratta. Una classe astratta non può essere istanziata direttamente.

Esempio di Classe Astratta

Nel nostro esempio, la classe Forma è una classe astratta poiché la funzione area è una funzione virtuale pura.

class Forma {
public:
    virtual void disegna() const = 0;  // Funzione virtuale pura
    virtual double area() const = 0;   // Funzione virtuale pura
};

Vantaggi del Polimorfismo

  • Flessibilità: Il polimorfismo permette di scrivere codice flessibile e riutilizzabile, in grado di lavorare con diversi tipi di dati attraverso un’interfaccia comune.
  • Estensibilità: È facile estendere il sistema aggiungendo nuove classi derivate senza modificare il codice esistente.
  • Manutenibilità: Riduce la duplicazione del codice, migliorando la manutenibilità.

Limitazioni del Polimorfismo

  • Overhead di Prestazioni: L’uso del polimorfismo comporta un overhead di runtime, poiché le chiamate a funzioni virtuali richiedono il lookup nella vtable (tabella virtuale).
  • Complessità: L’implementazione e la gestione del polimorfismo possono aumentare la complessità del sistema, specialmente in gerarchie di classi complesse.

Best Practices

  • Usare il polimorfismo quando è necessario: Non abusare delle funzioni virtuali; utilizzale solo quando è necessario un comportamento diverso nelle classi derivate.
  • Gestione della memoria: Quando usi il polimorfismo con puntatori, assicurati di gestire correttamente la memoria, preferibilmente usando smart pointers come std::unique_ptr o std::shared_ptr.
  • Documentare l’uso delle funzioni virtuali: Le funzioni virtuali dovrebbero essere ben documentate, soprattutto quando sono virtuali pure, per chiarire il contratto che le classi derivate devono rispettare.

Conclusione

Il polimorfismo è uno strumento potente in C++ che permette di scrivere codice flessibile, estensibile e più facile da mantenere. Comprendere come implementare e utilizzare correttamente il polimorfismo tramite funzioni virtuali e classi astratte è fondamentale per qualsiasi sviluppatore C++ che desideri sfruttare al massimo la programmazione orientata agli oggetti. Con un uso accorto e ben pianificato del polimorfismo, è possibile creare sistemi complessi che rimangono flessibili e facili da estendere nel tempo.