Recherche…


Définir des classes polymorphes

L'exemple typique est une classe de forme abstraite, qui peut ensuite être dérivée en carrés, cercles et autres formes concrètes.

La classe parente:

Commençons par la classe polymorphe:

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

Comment lire cette définition?

  • Vous pouvez définir un comportement polymorphe par des fonctions membres introduites avec le mot-clé virtual . get_surface() et describe_object() seront évidemment implémentés différemment pour un carré que pour un cercle. Lorsque la fonction est appelée sur un objet, la fonction correspondant à la classe réelle de l'objet sera déterminée au moment de l'exécution.

  • Cela n'a aucun sens de définir get_surface() pour une forme abstraite. C'est pourquoi la fonction est suivie de = 0 . Cela signifie que la fonction est une fonction virtuelle pure .

  • Une classe polymorphe doit toujours définir un destructeur virtuel.

  • Vous pouvez définir des fonctions membres non virtuelles. Lorsque ces fonctions seront invoquées pour un objet, la fonction sera choisie en fonction de la classe utilisée au moment de la compilation. get_double_surface() est défini de cette manière.

  • Une classe contenant au moins une fonction virtuelle pure est une classe abstraite. Les classes abstraites ne peuvent pas être instanciées. Vous ne pouvez avoir que des pointeurs ou des références d'un type de classe abstrait.

Classes dérivées

Une fois qu'une classe de base polymorphe est définie, vous pouvez la dériver. Par exemple:

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

Quelques explications:

  • Vous pouvez définir ou remplacer l'une des fonctions virtuelles de la classe parente. Le fait qu'une fonction soit virtuelle dans la classe parente le rend virtuel dans la classe dérivée. Pas besoin de redire le mot-clé virtual au compilateur. Mais il est recommandé d'ajouter le mot-clé override à la fin de la déclaration de la fonction, afin d'éviter les bogues subtils causés par des variations inaperçues de la signature de la fonction.
  • Si toutes les fonctions virtuelles pures de la classe parente sont définies, vous pouvez instancier des objets pour cette classe, sinon elle deviendra également une classe abstraite.
  • Vous n'êtes pas obligé de remplacer toutes les fonctions virtuelles. Vous pouvez conserver la version du parent si cela vous convient.

Exemple d'instanciation

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

Downcasting sûr

Supposons que vous ayez un pointeur sur un objet d'une classe polymorphe:

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 abaissement consisterait à convertir une Shape polymorphe générale en une Shape dérivée et plus spécifique, comme Square ou Circle .

Pourquoi baisser les bras?

La plupart du temps, vous n'avez pas besoin de savoir quel est le type réel de l'objet, car les fonctions virtuelles vous permettent de manipuler votre objet indépendamment de son type:

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

Si vous n'avez pas besoin de baisses, votre conception serait parfaite.

Cependant, il se peut que vous deviez parfois baisser. Un exemple typique est lorsque vous souhaitez appeler une fonction non virtuelle qui n'existe que pour la classe enfant.

Prenons par exemple les cercles. Seuls les cercles ont un diamètre. Ainsi, la classe serait définie comme suit:

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 fonction membre get_diameter() n'existe que pour les cercles. Il n'a pas été défini pour un objet Shape :

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

Comment baisser les bras?

Si vous savez avec certitude que ps pointe sur un cercle, vous pouvez opter pour un static_cast :

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

Ça fera l'affaire. Mais c'est très risqué: si ps apparaît par autre chose qu'un Circle le comportement de votre code sera indéfini.

Donc, plutôt que de jouer à la roulette russe, vous devez utiliser un dynamic_cast toute sécurité. Ceci est spécifiquement pour les classes polymorphes:

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

Notez que dynamic_cast n'est pas possible sur une classe qui n'est pas polymorphe. Vous devez avoir au moins une fonction virtuelle dans la classe ou ses parents pour pouvoir l'utiliser.

Polymorphisme & Destructeurs

Si une classe est destinée à être utilisée de manière polymorphe, les instances dérivées étant stockées en tant que pointeurs / références de base, le destructeur de sa classe de base doit être virtual ou protected . Dans le premier cas, cela provoquera la destruction de l'objet par la vtable , en appelant automatiquement le destructeur correct en fonction du type dynamique. Dans ce dernier cas, la destruction de l'objet via un pointeur / référence de classe de base est désactivée et l'objet ne peut être supprimé que lorsqu'il est explicitement traité comme son type réel.

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.

Ces deux pratiques garantissent que le destructeur de la classe dérivée sera toujours appelé sur les instances de classe dérivées, empêchant les fuites de mémoire.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow