Buscar..


Definir clases polimórficas.

El ejemplo típico es una clase de forma abstracta, que luego puede derivarse en cuadrados, círculos y otras formas concretas.

La clase padre:

Empecemos con la clase polimórfica:

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

¿Cómo leer esta definición?

  • Puede definir el comportamiento polimórfico mediante funciones miembro introducidas con la palabra clave virtual . Aquí, get_surface() y describe_object() obviamente se implementarán de manera diferente para un cuadrado que para un círculo. Cuando se invoca la función en un objeto, la función correspondiente a la clase real del objeto se determinará en el tiempo de ejecución.

  • No tiene sentido definir get_surface() para una forma abstracta. Es por esto que a la función le sigue = 0 . Esto significa que la función es pura función virtual .

  • Una clase polimórfica siempre debe definir un destructor virtual.

  • Puede definir funciones miembro no virtuales. Cuando estas funciones se invocarán para un objeto, la función se elegirá en función de la clase utilizada en el momento de la compilación. Aquí get_double_surface() se define de esta manera.

  • Una clase que contiene al menos una función virtual pura es una clase abstracta. Las clases abstractas no pueden ser instanciadas. Solo puede tener punteros o referencias de un tipo de clase abstracta.

Clases derivadas

Una vez que se define una clase base polimórfica, se puede derivar. Por ejemplo:

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

Algunas explicaciones:

  • Puede definir o anular cualquiera de las funciones virtuales de la clase principal. El hecho de que una función fuera virtual en la clase principal la hace virtual en la clase derivada. No hay necesidad de decirle al compilador la palabra clave virtual nuevo. Pero se recomienda agregar la override la palabra clave al final de la declaración de la función, a fin de evitar errores sutiles causados ​​por variaciones inadvertidas en la firma de la función.
  • Si se definen todas las funciones virtuales puras de la clase padre, puede crear una instancia de los objetos para esta clase, de lo contrario, también se convertirá en una clase abstracta.
  • No está obligado a anular todas las funciones virtuales. Puede conservar la versión del padre si se adapta a sus necesidades.

Ejemplo de instanciación

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

Descenso seguro

Supongamos que tiene un puntero a un objeto de una clase polimórfica:

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

una caída hacia abajo sería desde una Shape polimórfica general hasta una de sus formas derivadas y más específicas, como Square o Circle .

¿Por qué abatirse?

La mayoría de las veces, no necesitará saber cuál es el tipo real del objeto, ya que las funciones virtuales le permiten manipular su objeto independientemente de su tipo:

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

Si no necesita ningún abatimiento, su diseño sería perfecto.

Sin embargo, es posible que a veces tenga que abatirse. Un ejemplo típico es cuando desea invocar una función no virtual que existe solo para la clase secundaria.

Consideremos por ejemplo los círculos. Sólo los círculos tienen un diámetro. Así que la clase se definiría como:

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 función miembro get_diameter() solo existe para círculos. No se definió para un objeto Shape :

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

¿Cómo abatir?

Si static_cast seguro de que ps apunta a un círculo, podría optar por un static_cast :

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

Esto hará el truco. Pero es muy arriesgado: si ps aparece por algo más que un Circle el comportamiento de su código será indefinido.

Así que, en lugar de jugar a la ruleta rusa, debes usar de forma segura un dynamic_cast . Esto es específicamente para las clases polimórficas:

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

Tenga en cuenta que dynamic_cast no es posible en una clase que no sea polimórfica. Necesitaría al menos una función virtual en la clase o sus padres para poder usarla.

Polimorfismo y Destructores

Si se pretende que una clase se utilice de forma polimórfica, con las instancias derivadas almacenadas como punteros / referencias base, el destructor de su clase base debe ser virtual o estar protected . En el primer caso, esto causará que la destrucción del objeto verifique el vtable , llamando automáticamente al destructor correcto según el tipo dinámico. En este último caso, la destrucción del objeto a través de un puntero / referencia de clase base está deshabilitada, y el objeto solo se puede eliminar cuando se trata explícitamente como su tipo real.

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.

Ambas prácticas garantizan que siempre se llamará al destructor de la clase derivada en las instancias de la clase derivada, lo que evita las fugas de memoria.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow