17 votes

Obliger les fonctions virtuelles surchargées à appeler les implémentations de base

Dans une hiérarchie de classes C++, est-il possible d'imposer qu'une fonction virtuelle particulière appelle toujours l'implémentation de sa classe de base ? (Comme la façon dont les constructeurs s'enchaînent ?)

J'étudie le cas d'une hiérarchie de classes profonde qui possède des fonctions d'interface communes que chaque enfant remplace. J'aimerais que la surcharge de chaque classe dérivée se répercute sur la classe de base. Il est facile de faire cela explicitement avec la fonction par exemple le code ci-dessous, mais il y a le risque que quelqu'un implémentant une nouvelle classe dérivée puisse oublier de chaîner à la base.

Existe-t-il un modèle permettant de faire respecter cette règle, de sorte que le compilateur génère une erreur si une surcharge ne parvient pas à enchaîner la base ?

Ainsi, en

class CAA 
{
   virtual void OnEvent( CEvent *e ) { 
     // do base implementation stuff;
   }
}

class CBB : public CAA
{
   typedef CAA BaseClass;
   virtual void OnEvent( CEvent *e ) { 
       DoCustomCBBStuff();
       BaseClass::OnEvent( e ); // chain to base
   }
}

class CCC : public CBB
{
   typedef CBB BaseClass;
   virtual void OnEvent( CEvent *e ) { 
       Frobble();
       Glorp();
       BaseClass::OnEvent( e ); // chain to CBB which chains to CAA, etc
   }
}

class CDD : public CCC
{
   typedef CCC BaseClass;
   virtual void OnEvent( CEvent *e ) { 
       Meep();
       // oops! forgot to chain to base!
   }
}

Y a-t-il un moyen, une astuce de template ou de syntaxe, pour que CDD affiche une erreur plus évidente ?

9voto

Joshua Points 13231

La méthode de la classe de base n'est pas virtuelle et appelle une méthode virtuelle protégée.

Bien sûr, cela ne gère qu'un seul niveau.

Dans votre situation particulière, une grande quantité d'infrastructure peut faire fonctionner le système, mais cela n'en vaut pas la peine.

La réponse typique est d'ajouter un commentaire

// Always call base class method

3voto

Aaron McDaid Points 7761

Placez un type spécial "caché" dans la classe Base avec un constructeur privé, en utilisant la fonction friend pour s'assurer que seule la base peut le créer. Ce code sur ideone .

S'il y a plusieurs niveaux, cela ne garantit malheureusement pas que la classe de base immédiate soit appelée. Par conséquent, struct E : public D; pourrait mettre en œuvre E::foo() avec un appel à B:: foo alors que vous pourriez préférer un appel à D::foo() .

struct Base {
        struct Hidden {
                friend class Base;
                private:
                        Hidden() {}
        };
        virtual Hidden foo() {
                cout << "Base" << endl;
                return Hidden(); // this can create a Hidden
        }
};

struct D : public B {
        virtual Hidden foo() {
                cout << "D" << endl;
                // return Hidden(); // error as the constructor is private from here
                return B :: foo();
        }
};

Si vous avez essayé d'implémenter D : : foo() sans retour ou avec return Hidden() vous obtiendrez un message d'erreur. La seule façon de compiler ceci est d'utiliser return B :: foo() .

3voto

elmo Points 633

En suivant une règle simple pour dériver à travers une classe modèle, c'est possible.

#include <iostream>

struct TEvent
{
};

struct Base {
    virtual void CallOnEvent(TEvent * e)
    {
        OnEvent(e);
    }
    virtual void OnEvent(TEvent * e)
    {
        std::cout << "Base::Event" << std::endl;
    }
    void CallUp(TEvent * e)
    {
    }

};

template <typename B>
struct TDerived : public B
{
    void CallUp( TEvent * e )
    {
        B::CallUp(e);
        B::OnEvent(e);
    }
    virtual void CallOnEvent( TEvent * e )
    {
        CallUp(e);
        this->OnEvent(e);
    }
};

struct Derived01 : public TDerived< Base >
{
    void OnEvent(TEvent * e)
    {
        std::cout << "Derived01::Event" << std::endl;
    }
};

struct Derived02 : public TDerived< Derived01 >
{
    void OnEvent(TEvent * e)
    {
        std::cout << "Derived02::Event" << std::endl;
    }
};

struct Derived03 : public TDerived< Derived02 >
{
    void OnEvent(TEvent * e)
    {
        std::cout << "Derived03::Event" << std::endl;
    }
};

struct Derived04 : public TDerived< Derived03 >
{
    void OnEvent(TEvent * e)
    {
        std::cout << "Derived04::Event" << std::endl;
    }
};

int main( void )
{
 Derived04 lD4;
 lD4.CallOnEvent(0);
 return 0;
}

Ce code donne ( codepad ):

Base::Event
Derived01::Event
Derived02::Event
Derived03::Event
Derived04::Event

Concernant certaines réponses utilisant typeid . Je n'envisagerais jamais d'utiliser typeid pour autre chose que le débogage. Cela est dû à deux choses :

  • la vérification dynamique des types peut être effectuée d'une manière beaucoup plus efficace (sans créer d'erreur de type). type_info c'est-à-dire en utilisant dynamic_cast certaines méthodes
  • La norme C++ ne garantit que l'existence du typeid, mais pas vraiment la façon dont il fonctionne (la plupart des choses sont spécifiques au compilateur).

editar:

Un exemple légèrement plus complexe avec un héritage multiple. Celui-ci n'est malheureusement pas soluble sans appels explicites dans les classes qui héritent de bases multiples (principalement parce que ce qui doit se passer dans de tels cas n'est pas clair, nous devons donc définir explicitement le comportement).

#include <iostream>

struct TEvent
{
};

struct Base {
    virtual void CallOnEvent(TEvent * e)
    {
        OnEvent(e);
    }
    virtual void OnEvent(TEvent * e)
    {
        std::cout << "Base::Event" << std::endl;
    }

    void CallUp(TEvent * e)
    {
    }
};

template <typename B >
struct TDerived : public B
{
    void CallUp( TEvent * e )
    {
        B::CallUp(e);
        B::OnEvent(e);
    }
    virtual void CallOnEvent( TEvent * e )
    {
        CallUp(e);
        this->OnEvent(e);
    }
};

struct Derived01 : virtual public TDerived< Base >
{
    void OnEvent(TEvent * e)
    {
        std::cout << "Derived01::Event" << std::endl;
    }
};

struct Derived02 : virtual public TDerived< Derived01 >
{
    void OnEvent(TEvent * e)
    {
        std::cout << "Derived02::Event" << std::endl;
    }
};

typedef TDerived< Derived02 > TDerived02;
typedef TDerived< Derived01 > TDerived01;
struct Derived03 : virtual public TDerived02, virtual public TDerived01
{
    void OnEvent(TEvent * e)
    {
        std::cout << "Derived03::Event" << std::endl;
    }

    virtual void CallOnEvent( TEvent * e )
    {
        CallUp(e);
        Derived03::OnEvent(e);
    }
    void CallUp( TEvent * e )
    {
        TDerived02::CallUp(e);
        TDerived01::CallUp(e);
    }
};

struct Derived04 : public TDerived< Derived03 >
{
    void OnEvent(TEvent * e)
    {
        std::cout << "Derived04::Event" << std::endl;
    }
};

int main( void )
{
 Derived04 lD4;
 Derived03 lD3;

 lD3.CallOnEvent( 0 );
 std::cout << std::endl;
 lD4.CallOnEvent( 0 );

 return ( 0 );
}

Le résultat est ( idéone ):

Base::Event      \                  \
Derived01::Event | - from Derived02 |
Derived02::Event /                  |-- from Derived03
Base::Event      \__ from Derived01 |
Derived01::Event /                  |
Derived03::Event                    /

Base::Event      \                  \                  \
Derived01::Event | - from Derived02 |                  |
Derived02::Event /                  |-- from Derived03 |-- from Derived04
Base::Event      \__ from Derived01 |                  |
Derived01::Event /                  |                  |
Derived03::Event                    /                  |
Derived04::Event                                       /

1voto

Seth Carnegie Points 45196

Il n'y a pas de support pour cela dans le langage C++, mais en développant le commentaire de KerrekSB, vous pourriez faire quelque chose comme ceci :

class A {
public:
    void DoEvent(int i) {
        for (auto event = events.begin(); event != events.end(); ++event)
            (this->*(*event))(i);
    }

protected:
    typedef void (A::*Event)(int);

    A(Event e) {
        events.push_back(&A::OnEvent);
        events.push_back(e);
    }

    void OnEvent(int i) {
        cout << "A::OnEvent " << i << endl;
    }

    vector<Event> events;
};

class B : public A {
public:
    B() : A((Event)&B::OnEvent) { }

protected:
    B(Event e) : A((Event)&B::OnEvent) {
        events.push_back(e);
    }

    void OnEvent(int i) {
        cout << "B::OnEvent " << i << endl;
    }
};

class C : public B {
public:
    C() : B((Event)&C::OnEvent) { }

protected:
    C(Event e) : B((Event)&C::OnEvent) {
        events.push_back(e);
    }

    void OnEvent(int i) {
        cout << "C::OnEvent " << i << endl;
    }
};

Ensuite, utilisez-le comme ceci

int main() {
    A* ba = new B;
    ba->DoEvent(32);

    B* bb = new B;
    bb->DoEvent(212);

    A* ca = new C;
    ca->DoEvent(44212);

    B* cb = new C;
    cb->DoEvent(2);

    C* cc = new C;
    cc->DoEvent(9);
}

Ces résultats

A::OnEvent 32
B::OnEvent 32

A::OnEvent 212
B::OnEvent 212

A::OnEvent 44212
B::OnEvent 44212
C::OnEvent 44212

A::OnEvent 2
B::OnEvent 2
C::OnEvent 2

A::OnEvent 9
B::OnEvent 9
C::OnEvent 9

Vous devez faire un peu de travail, mais vous ne devez pas appeler manuellement la fonction membre baser à la fin de chaque appel. Voici la démo en direct .

0voto

Dietmar Kühl Points 70604

Il n'y a rien qui oblige directement une fonction de surcharge à faire quelque chose en particulier, à part retourner un certain type. Cependant, si vous rendez la fonction virtuelle de la classe de base private aucune fonction ne peut l'appeler sur la classe de base mais les classes dérivées peuvent la surcharger. Vous fournissez alors également un public qui appelle la fonction virtuelle ainsi qu'une fonction effectuant la logique de la classe de base. La logique de la classe de base devrait probablement aller dans une fonction séparée (éventuellement la fonction de transfert non virtuelle directement) pour éviter de l'exécuter deux fois si l'objet se trouve être un objet de base.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X