Ricerca…


Definire classi polimorfiche

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;
}

Downcast sicuro

Supponiamo di avere un puntatore a un oggetto di una classe polimorfica:

Shape *ps;                       // see example on defining a polymorphic class
ps =  get_a_new_random_shape();  // if you don't have such a function yet, you 
                                 // could just write ps = new Square(0.0,0.0, 5);

un downcast sarebbe quello di lanciare da una Shape polimorfica generale a una delle sue forme derivate e più specifiche come Square o Circle .

Perché downcast?

Nella maggior parte dei casi, non è necessario sapere quale sia il tipo reale dell'oggetto, poiché le funzioni virtuali consentono di manipolare il tuo oggetto indipendentemente dal suo tipo:

std::cout << "Surface: " << ps->get_surface() << std::endl; 

Se non hai bisogno di alcun downcast, il tuo design sarebbe perfetto.

Tuttavia, a volte potrebbe essere necessario downcast. Un tipico esempio è quando si desidera richiamare una funzione non virtuale che esiste solo per la classe figlio.

Prendi in considerazione per esempio i cerchi. Solo i cerchi hanno un diametro. Quindi la classe verrebbe definita come:

class Circle: public Shape { // for Shape, see example on defining a polymorphic class
    Point center;
    double radius;
public: 
    Circle (const Point& center, double radius)
       : center(center), radius(radius) {}

    double get_surface() const override { return r * r * M_PI; }   

    // this is only for circles. Makes no sense for other shapes 
    double get_diameter() const { return 2 * r; }
};

La funzione membro get_diameter() esiste solo per le cerchie. Non è stato definito per un oggetto Shape :

Shape* ps = get_any_shape();
ps->get_diameter(); // OUCH !!! Compilation error 

Come abbattere?

Se sapessi per certo che ps indica una cerchia puoi optare per un static_cast :

std::cout << "Diameter: " << static_cast<Circle*>(ps)->get_diameter() << std::endl;

Questo farà il trucco. Ma è molto rischioso: se ps sembra essere diverso da un Circle il comportamento del tuo codice sarà indefinito.

Quindi, piuttosto che giocare alla roulette russa, dovresti usare tranquillamente un dynamic_cast . Questo è specifico per le classi polimorfiche:

int main() {
    Circle circle(Point(0.0, 0.0), 10);
    Shape &shape = circle;

    std::cout << "The shape has a surface of " << shape.get_surface() << std::endl;

    //shape.get_diameter();   // OUCH !!! Compilation error 

    Circle *pc = dynamic_cast<Circle*>(&shape); // will be nullptr if ps wasn't a circle 
    if (pc) 
        std::cout << "The shape is a circle of diameter " << pc->get_diameter() << std::endl;
    else
        std::cout << "The shape isn't a circle !" << std::endl; 
}        

Nota che dynamic_cast non è possibile su una classe che non è polimorfica. Avresti bisogno di almeno una funzione virtuale nella classe o dei suoi genitori per poterla usare.

Polimorfismo e distruttori

Se si intende utilizzare una classe in modo polimorfico, con le istanze derivate memorizzate come puntatori / riferimenti di base, il distruttore della sua classe base deve essere virtual o protected . Nel primo caso, questo causerà la distruzione dell'oggetto per controllare il vtable , chiamando automaticamente il distruttore corretto in base al tipo dinamico. In quest'ultimo caso, la distruzione dell'oggetto tramite un puntatore / riferimento di classe base è disabilitata e l'oggetto può essere eliminato solo se trattato esplicitamente come tipo effettivo.

struct VirtualDestructor {
    virtual ~VirtualDestructor() = default;
};

struct VirtualDerived : VirtualDestructor {};

struct ProtectedDestructor {
  protected:
    ~ProtectedDestructor() = default;
};

struct ProtectedDerived : ProtectedDestructor {
    ~ProtectedDerived() = default;
};

// ...

VirtualDestructor* vd = new VirtualDerived;
delete vd; // Looks up VirtualDestructor::~VirtualDestructor() in vtable, sees it's
           // VirtualDerived::~VirtualDerived(), calls that.

ProtectedDestructor* pd = new ProtectedDerived;
delete pd; // Error: ProtectedDestructor::~ProtectedDestructor() is protected.
delete static_cast<ProtectedDerived*>(pd); // Good.

Entrambe queste pratiche garantiscono che il distruttore della classe derivata verrà sempre chiamato su istanze di classi derivate, impedendo perdite di memoria.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow