web-dev-qa-db-fra.com

appel de la fonction virtuelle pure depuis le constructeur de la classe de base

J'ai une classe de base MyBase qui contient une fonction virtuelle pure:

void PrintStartMessage() = 0

Je veux que chaque classe dérivée l'appelle dans son constructeur

puis je le mets dans le constructeur de la classe de base (MyBase)

 class MyBase
 {
 public:

      virtual void PrintStartMessage() =0;
      MyBase()
      {
           PrintStartMessage();
      }

 };

 class Derived:public MyBase
 {     

 public:
      void  PrintStartMessage(){

      }
 };

void main()
 {
      Derived derived;
 }

mais je reçois une erreur de l'éditeur de liens.

 this is error message : 

 1>------ Build started: Project: s1, Configuration: Debug Win32 ------
 1>Compiling...
 1>s1.cpp
 1>Linking...
 1>s1.obj : error LNK2019: unresolved external symbol "public: virtual void __thiscall MyBase::PrintStartMessage(void)" (?PrintStartMessage@MyBase@@UAEXXZ) referenced in function "public: __thiscall MyBase::MyBase(void)" (??0MyBase@@QAE@XZ)
 1>C:\Users\Shmuelian\Documents\Visual Studio 2008\Projects\s1\Debug\s1.exe : fatal error LNK1120: 1 unresolved externals
 1>s1 - 2 error(s), 0 warning(s)

Je veux forcer toutes les classes dérivées à ...

A- implement it

B- call it in their constructor 

Comment je dois le faire?

32
herzl shemuelian

De nombreux articles expliquent pourquoi vous ne devez jamais appeler des fonctions virtuelles dans un constructeur et un destructeur en C++. Jetez un coup d'œil ici et ici pour plus de détails sur ce qui se passe en coulisse lors de tels appels.

En bref, les objets sont construits de la base au dérivé. Ainsi, lorsque vous essayez d'appeler une fonction virtuelle à partir du constructeur de la classe de base, le remplacement des classes dérivées n'a pas encore eu lieu car les constructeurs dérivés n'ont pas encore été appelés.

30
a1ex07

Il est dangereux d’appeler une méthode abstraite pure à partir d’une méthode dérivée pendant que cet objet est encore en cours de construction. C'est comme essayer de faire le plein d'essence dans une voiture, mais cette voiture est toujours sur la chaîne de montage et le réservoir d'essence n'a pas encore été installé.

Le mieux que vous puissiez faire est de construire d'abord votre objet, puis d'appeler la méthode après:

template <typename T>
T construct_and_print()
{
  T obj;
  obj.PrintStartMessage();

  return obj;
}

int main()
{
    Derived derived = construct_and_print<Derived>();
}
13
greatwolf

Vous ne pouvez pas le faire comme vous l'imaginez, car vous ne pouvez pas appeler des fonctions virtuelles dérivées à partir du constructeur de la classe de base. L'objet n'est pas encore du type dérivé. Mais vous n'avez pas besoin de faire ça.

Appel de PrintStartMessage après la construction de MyBase

Supposons que vous vouliez faire quelque chose comme ceci:

class MyBase {
public:
    virtual void PrintStartMessage() = 0;
    MyBase() {
        printf("Doing MyBase initialization...\n");
        PrintStartMessage(); // ⚠ UB: pure virtual function call ⚠
    }
};

class Derived : public MyBase {
public:
    virtual void PrintStartMessage() { printf("Starting Derived!!!\n"); }
};

La trace d'exécution souhaitée serait:

Doing MyBase initialization...
Starting Derived!!!

Mais c’est à ça que servent les constructeurs! Il suffit de supprimer la fonction virtuelle et de faire en sorte que le constructeur de la dérivée fasse le travail!

class MyBase {
public:
    MyBase() { printf("Doing MyBase initialization...\n"); }
};

class Derived : public MyBase {
public:
    Derived() { printf("Starting Derived!!!\n"); }
};

Le résultat est, bien, ce que nous attendions:

Doing MyBase initialization...
Starting Derived!!!

Cela n’impose cependant pas les classes dérivées pour implémenter explicitement la fonctionnalité PrintStartMessage. Mais d’un autre côté, réfléchissez-y à deux fois si cela est nécessaire, car sinon, ils peuvent toujours fournir une implémentation vide.

Appel de PrintStartMessage avant la construction de MyBase

Comme indiqué ci-dessus, si vous devez appeler PrintStartMessage avant que le Derived ait été constructeur, vous ne pouvez pas le faire car il n'y a pas encore d'objet Derived pour lequel PrintStartMessage doit être appelé. Cela n'aurait aucun sens d'exiger que PrintStartMessage soit un membre non statique, car il n'aurait aucun accès à aucun des membres de données Derived.

Une fonction statique avec la fonction d'usine

Alternativement, nous pouvons en faire un membre statique comme suit:

class MyBase {
public:
    MyBase() {
        printf("Doing MyBase initialization...\n");
    }
};

class Derived : public MyBase {
public:
    static void PrintStartMessage() { printf("Derived specific message.\n"); }
};

Une question naturelle se pose de savoir comment on va l'appeler.

Je peux voir qu'il existe deux solutions: l'une est similaire à celle de @greatwolf, où vous devez l'appeler manuellement. Mais maintenant, puisqu'il s'agit d'un membre statique, vous pouvez l'appeler avant qu'une instance de MyBase ait été construite:

template<class T>
T print_and_construct() {
    T::PrintStartMessage();
    return T();
}

int main() {
    Derived derived = print_and_construct<Derived>();
}

La sortie sera

Derived specific message.
Doing MyBase initialization...

Cette approche force toutes les classes dérivées à implémenter PrintStartMessage. Malheureusement, ce n’est vrai que lorsque nous les construisons avec notre fonction d’usine ... ce qui est un inconvénient majeur de cette solution.

La deuxième solution consiste à recourir au modèle de modèle curieusement récurrent (CRTP). En indiquant MyBase le type d'objet complet à la compilation, il peut effectuer l'appel à partir du constructeur:

template<class T>
class MyBase {
public:
    MyBase() {
        T::PrintStartMessage();
        printf("Doing MyBase initialization...\n");
    }
};

class Derived : public MyBase<Derived> {
public:
    static void PrintStartMessage() { printf("Derived specific message.\n"); }
};

La sortie est conforme aux attentes, sans qu'il soit nécessaire d'utiliser une fonction d'usine dédiée.

Accéder à MyBase à partir de PrintStartMessage avec CRTP

Lorsque MyBase est en cours d'exécution, son accès aux membres est déjà correct. Nous pouvons faire en sorte que PrintStartMessage puisse accéder à la MyBase qui l’a appelée:

template<class T>
class MyBase {
public:
    MyBase() {
        T::PrintStartMessage(this);
        printf("Doing MyBase initialization...\n");
    }
};

class Derived : public MyBase<Derived> {
public:
    static void PrintStartMessage(MyBase<Derived> *p) {
        // We can access p here
        printf("Derived specific message.\n");
    }
};

Ce qui suit est également valide et très fréquemment utilisé, même s’il est un peu dangereux:

template<class T>
class MyBase {
public:
    MyBase() {
        static_cast<T*>(this)->PrintStartMessage();
        printf("Doing MyBase initialization...\n");
    }
};

class Derived : public MyBase<Derived> {
public:
    void PrintStartMessage() {
        // We can access *this member functions here, but only those from MyBase
        // or those of Derived who follow this same restriction. I.e. no
        // Derived data members access as they have not yet been constructed.
        printf("Derived specific message.\n");
    }
};

Pas de solution de templates - refonte

Une autre option consiste à redéfinir un peu votre code. IMO celui-ci est en fait la solution préférée si vous devez absolument appeler une PrintStartMessage surchargée à partir de la construction MyBase.

Cette proposition consiste à séparer Derived de MyBase comme suit:

class ICanPrintStartMessage {
public:
    virtual ~ICanPrintStartMessage() {}
    virtual void PrintStartMessage() = 0;
};

class MyBase {
public:
    MyBase(ICanPrintStartMessage *p) : _p(p) {
        _p->PrintStartMessage();
        printf("Doing MyBase initialization...\n");
    }

    ICanPrintStartMessage *_p;
};

class Derived : public ICanPrintStartMessage {
public:
    virtual void PrintStartMessage() { printf("Starting Derived!!!\n"); }
};

Vous initialisez MyBase comme suit:

int main() {
    Derived d;
    MyBase b(&d);
}
7
ybungalobill

Vous ne devriez pas appeler une fonction virtual dans un constructeur. Période . Vous devrez trouver une solution de contournement, comme créer PrintStartMessage non -virtual et placer l'appel explicitement dans chaque constructeur.

5
Fred Foo

Je sais que c'est une vieille question, mais je suis tombé sur la même question en travaillant sur mon programme.

Si votre objectif est de réduire la duplication de code en demandant à la classe de base de gérer le code d'initialisation partagé tout en demandant aux classes dérivées de spécifier le code qui leur est propre dans une méthode virtuelle pure, c'est ce que j'ai décidé.

#include <iostream>

class MyBase
{
public:
    virtual void UniqueCode() = 0;
    MyBase() {};
    void init(MyBase & other)
    {
      std::cout << "Shared Code before the unique code" << std::endl;
      other.UniqueCode();
      std::cout << "Shared Code after the unique code" << std::endl << std::endl;
    }
};

class FirstDerived : public MyBase
{
public:
    FirstDerived() : MyBase() { init(*this); };
    void  UniqueCode()
    {
      std::cout << "Code Unique to First Derived Class" << std::endl;
    }
private:
    using MyBase::init;
};

class SecondDerived : public MyBase
{
public:
    SecondDerived() : MyBase() { init(*this); };
    void  UniqueCode()
    {
      std::cout << "Code Unique to Second Derived Class" << std::endl;
    }
private:
    using MyBase::init;
};

int main()
{
    FirstDerived first;
    SecondDerived second;
}

La sortie est:

 Shared Code before the unique code
 Code Unique to First Derived Class
 Shared Code after the unique code

 Shared Code before the unique code
 Code Unique to Second Derived Class
 Shared Code after the unique code
0
Circuitrinos

Si PrintStartMessage () n'était pas une fonction virtuelle pure mais une fonction virtuelle normale, le compilateur ne s'en plaindrait pas. Cependant, vous devez toujours comprendre pourquoi la version dérivée de PrintStartMessage () n'est pas appelée. 

Comme la classe dérivée appelle le constructeur de la classe de base avant son propre constructeur, la classe dérivée se comporte comme la classe de base et appelle donc la fonction de la classe de base. 

0
fatma.ekici