C++ Definire classi polimorfiche


Esempio

L'esempio tipico è una classe di forma astratta, che può quindi essere derivata in quadrati, cerchi e altre forme concrete.

La classe genitore:

Iniziamo con la classe polimorfica:

class Shape {
public:
    virtual ~Shape() = default;
    virtual double get_surface() const = 0;
    virtual void describe_object() const { std::cout << "this is a shape" << std::endl; }  

    double get_doubled_surface() const { return 2 * get_surface(); } 
};

Come leggere questa definizione?

  • È possibile definire il comportamento polimorfico mediante funzioni membro introdotte con la parola chiave virtual . Qui get_surface() e describe_object() saranno ovviamente implementati in modo diverso per un quadrato che per un cerchio. Quando la funzione viene invocata su un oggetto, la funzione corrispondente alla classe reale dell'oggetto sarà determinata in fase di esecuzione.

  • Non ha senso definire get_surface() per una forma astratta. Questo è il motivo per cui la funzione è seguita da = 0 . Ciò significa che la funzione è pura funzione virtuale .

  • Una classe polimorfica dovrebbe sempre definire un distruttore virtuale.

  • È possibile definire funzioni membro non virtuali. Quando queste funzioni saranno invocate per un oggetto, la funzione verrà scelta in base alla classe utilizzata in fase di compilazione. Qui get_double_surface() è definito in questo modo.

  • Una classe che contiene almeno una funzione virtuale pura è una classe astratta. Le classi astratte non possono essere istanziate. Potresti avere solo puntatori o riferimenti di un tipo di classe astratta.

Classi derivate

Una volta definita una classe di base polimorfica, è possibile derivarla. Per esempio:

class Square : public Shape {
    Point top_left;
    double side_length;
public: 
    Square (const Point& top_left, double side)
       : top_left(top_left), side_length(side_length) {}

    double get_surface() override { return side_length * side_length; }   
    void describe_object() override { 
        std::cout << "this is a square starting at " << top_left.x << ", " << top_left.y
                  << " with a length of " << side_length << std::endl; 
    }  
};

Alcune spiegazioni:

  • È possibile definire o sovrascrivere qualsiasi funzione virtuale della classe genitore. Il fatto che una funzione fosse virtuale nella classe genitore lo rende virtuale nella classe derivata. Non c'è bisogno di dire al compilatore di nuovo la parola chiave virtual . Tuttavia, si consiglia di aggiungere la override della parola chiave alla fine della dichiarazione della funzione, al fine di prevenire piccoli bug causati da variazioni non notate nella firma della funzione.
  • Se tutte le pure funzioni virtuali della classe genitrice sono definite, puoi istanziare oggetti per questa classe, altrimenti diventerà anche una classe astratta.
  • Non sei obbligato a scavalcare tutte le funzioni virtuali. Puoi mantenere la versione del genitore se è adatta alle tue necessità.

Esempio di istanziazione

int main() {

    Square square(Point(10.0, 0.0), 6); // we know it's a square, the compiler also
    square.describe_object(); 
    std::cout << "Surface: " << square.get_surface() << std::endl; 

    Circle circle(Point(0.0, 0.0), 5);

    Shape *ps = nullptr;  // we don't know yet the real type of the object
    ps = &circle;         // it's a circle, but it could as well be a square
    ps->describe_object(); 
    std::cout << "Surface: " << ps->get_surface() << std::endl;
}