OVH Cloud OVH Cloud

Appel de méthodes virtuelles pures dans un constructeur

12 réponses
Avatar
Hugues Delorme
Soit :

class A
{
public:
A () { do_initialize (); }

virtual void do_initialize () = 0;
};

class AImpl : public A
{
public:
AImpl () : A ()
{
}
void do_initialize ()
{
std::cout << "Initialization" << std::endl;
}
};

Compiler ce code avec g++ génère l'erreur suivante :
In constructor 'A::A()': abstract virtual 'virtual void A::do_initialize()'
called from constructor.

Il est impossible d'appeler des méthodes abstraites à partir d'un
constructeur de classe. Pourtant, ceci pourrait être très pratique pour
définir un schéma d'initialisation commun à tous les descendants(une sorte
de patron de méthode pour la création d'instances) :

Comment détourner le problème?, car devoir recopier le même code
d'initialisation dans les constructeurs des descendants rend le texte
logiciel redondant.

10 réponses

1 2
Avatar
Marc Boyer
Hugues Delorme wrote:
Soit :

class A
{
public:
A () { do_initialize (); }

virtual void do_initialize () = 0;
};

class AImpl : public A
{
public:
AImpl () : A ()
{
}
void do_initialize ()
{
std::cout << "Initialization" << std::endl;
}
};

Compiler ce code avec g++ génère l'erreur suivante :
In constructor 'A::A()': abstract virtual 'virtual void A::do_initialize()'
called from constructor.

Il est impossible d'appeler des méthodes abstraites à partir d'un
constructeur de classe.


Disons que, avant d'être un AImpl, ton objet est, pendant un cours
instant, un A. En gros, quand on construit un AImpl, on construit
d'abord un A (appel a A::A()), puis on rajoute la surcouche de AImpl
(appel de AImpl::AImpl()).
Le problème, c'est que quand ton objet est un A() et pas
encore un AImpl, do_initialize n'existe pas...

C'est logique dans le sens ou AImpl::do_initialize va
surement mettre à jour des attributs spécifiques de AImpl,
que A n'a pas (et donc que AImpl n'a pas encore quand il
n'est qu'un A).

Pourtant, ceci pourrait être très pratique pour
définir un schéma d'initialisation commun à tous les descendants(une sorte
de patron de méthode pour la création d'instances) :


Ben, n'est-ce pas le rôle de l'appel à A::A() ?

Comment détourner le problème?, car devoir recopier le même code
d'initialisation dans les constructeurs des descendants rend le texte
logiciel redondant.


Peux-tu préciser le problème ? Car le mécanisme de construction
tel qu'il existe me semble déjà résoudre le problème tel qu'énoncé
ci dessus (mais surement pas tel qu'il se présente à toi).

Marc Boyer
--
Lying for having sex or lying for making war? Trust US presidents :-(

Avatar
.oO LGV Oo.
Comment détourner le problème?, car devoir recopier le même code
d'initialisation dans les constructeurs des descendants rend le texte
logiciel redondant.


Peux-tu préciser le problème ? Car le mécanisme de construction
tel qu'il existe me semble déjà résoudre le problème tel qu'énoncé
ci dessus (mais surement pas tel qu'il se présente à toi).


Si je comprends bien, l'idée serait d'appeler automatiquement le
do_initialize() de l'objet dérivé à sa construction... ça peut paraitre
idiot ce que je vais dire, mais pourquoi ne pas faire le boulot de
do_initialize dans le constructeur de la classe dérivée ?! c'est quand meme
son role de construire correctement l'objet en tenant compte de ses
spécificités... :-?


Avatar
Marc Boyer
In article <bovp8f$fek$, .oO LGV Oo. wrote:
Comment détourner le problème?, car devoir recopier le même code
d'initialisation dans les constructeurs des descendants rend le texte
logiciel redondant.


Peux-tu préciser le problème ? Car le mécanisme de construction
tel qu'il existe me semble déjà résoudre le problème tel qu'énoncé
ci dessus (mais surement pas tel qu'il se présente à toi).


Si je comprends bien, l'idée serait d'appeler automatiquement le
do_initialize() de l'objet dérivé à sa construction... ça peut paraitre
idiot ce que je vais dire, mais pourquoi ne pas faire le boulot de
do_initialize dans le constructeur de la classe dérivée ?! c'est quand meme
son role de construire correctement l'objet en tenant compte de ses
spécificités... :-?


Ben oui, il me semble que c'est le rôle du constructeur.
En plus, on est sur que les constructeurs sont appelés dans l'ordre
de la hierarchie d'héritage.
C'est pour cela que je demandais un surplus d'explication
du problème.

Marc Boyer
--
Lying for having sex or lying for making war? Trust US presidents :-(



Avatar
Hugues Delorme
Pourtant, ceci pourrait être très pratique pour
définir un schéma d'initialisation commun à tous les descendants(une
sorte


de patron de méthode pour la création d'instances) :


Ben, n'est-ce pas le rôle de l'appel à A::A() ?


Bien sûr, mais pouvoir appeler des méthodes retardées dans le constructeur
de la classe ancêtre permettrait d'avoir une initialisation plus générique.
Dans l'exemple que j'ai donné, définir une méthode do_initialize() retardée
ne semble pas justifié vu la simplicité du code. Mais pour des classes plus
complexes, l'utilité me semble très intéressante.
Un autre exemple pour tenter d'illustrer le propos :

class B
{
public:
B (list<int>& ints)//patron de méthode
{
list<int>::iterator i = ints.begin ();
while (i != ints.end ())
{
do_append (*i);//appel primitive
i++;
}
}
virtual void do_append (int i) = 0;//primitive ajout
virtual void do_prune (int index) = 0;//primitive suppression
...
};

class BImpl : public B
{
public:
BImpl (list<int>& ints) : B (ints) { }
void do_append (int i)//implémentation de la primitive
{
ints.push_back (i);
}
private:
vector<int> ints;
};

Dans cet exemple, la classe B laisse à ses descendants le choix du type de
conteneur(vector, list, map, etc.) pour copier 'ints' qu'elle reçoit en
paramètre. B utilise les méthode retardées 'do_(...)' pour manipuler le
conteneur réel.
Mais ce genre de collaboration(issu du pattern patron de méthode) entre
ancêtre et descendants est impossible en C++ (autres langages je ne sais
pas), si le patron est implémenté dans un constructeur.

Comment détourner le problème?, car devoir recopier le même code
d'initialisation dans les constructeurs des descendants rend le texte
logiciel redondant.


Peux-tu préciser le problème ? Car le mécanisme de construction
tel qu'il existe me semble déjà résoudre le problème tel qu'énoncé
ci dessus (mais surement pas tel qu'il se présente à toi).


Le problème est bien celui-ci : il est impossible d'appeler des méthodes
virtuelles pures d'une classe dans le code de ses constructeurs.
En considérant le code ci-dessus, je suis obligé d'effectuer la copie de
'ints' dans le constructeur de BImpl :

BImpl (list<int>& ints) : B (ints)
{
list<int>::iterator i = ints.begin ();
while (i != ints.end ())
{
ints.push_back (*i);
i++;
}
}

Le code sera le même dans tous les constructeurs des descendants tels que
BImpl, à l'exception de la ligne 'ints.push_back' bien sûr : ce qui est bien
un cas de redondance.


Avatar
Jean-Marc Bourguet
"Hugues Delorme" writes:

Il est impossible d'appeler des méthodes abstraites à partir d'un
constructeur de classe.


Il est possible d'appeler des membres abstraits -- si tu les as
definis. (Note, le vocabulaire C++ ne connait pas de methodes.
Suivant les personnes, c'est n'importe quelle fonction membre ou bien
uniquement des membres virtuels).

Appeler des membres virtuels, directement ou indirectement, par un
constructeur ou un destructeur appelle la fonction correspondante de
l'objet construit. C'est pour eviter des problemes du genre:

struct A {
A() { f(); }
virtual void f() {}
};

struct B {
B() : A(), toto(5) {}
void f() { std::cout << toto << std::endl; }
int toto;
}

ou toto ne serait pas initialise lors de l'appel a f() par le
constructeur.

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org

Avatar
Benoit Rousseau
Hugues Delorme wrote:
Pourtant, ceci pourrait être très pratique pour
définir un schéma d'initialisation commun à tous les descendants(une



sorte

de patron de méthode pour la création d'instances) :


Ben, n'est-ce pas le rôle de l'appel à A::A() ?



Bien sûr, mais pouvoir appeler des méthodes retardées dans le constructeur
de la classe ancêtre permettrait d'avoir une initialisation plus générique.
Dans l'exemple que j'ai donné, définir une méthode do_initialize() retardée
ne semble pas justifié vu la simplicité du code. Mais pour des classes plus
complexes, l'utilité me semble très intéressante.
Un autre exemple pour tenter d'illustrer le propos :

class B
{
public:
B (list<int>& ints)//patron de méthode
{
list<int>::iterator i = ints.begin ();
while (i != ints.end ())
{
do_append (*i);//appel primitive
i++;
}
}
virtual void do_append (int i) = 0;//primitive ajout
virtual void do_prune (int index) = 0;//primitive suppression
...
};

class BImpl : public B
{
public:
BImpl (list<int>& ints) : B (ints) { }
void do_append (int i)//implémentation de la primitive
{
ints.push_back (i);
}
private:
vector<int> ints;
};

Dans cet exemple, la classe B laisse à ses descendants le choix du type de
conteneur(vector, list, map, etc.) pour copier 'ints' qu'elle reçoit en
paramètre. B utilise les méthode retardées 'do_(...)' pour manipuler le
conteneur réel.


Le problème, c'est que les variables de BImpl ne sont pas initialisées
quand tu appelles le constructeur de B :

class A {
public:
A() {
cout<<"A()"<<endl;
}

};

class C {
public:
C() { cout<<"C()"<<endl; }
};

class B : public A {
public:
B() : c(), A() { cout << "B()" << endl; };
C c;
};

affichera
A()
C()
B()
quelquesoit l'ordre de A() et c() apres B():

Quel est, d'ailleur, l'ordre d'appel des constructeurs dans le cas
d'heritage multiple ?


--
--------------------------------------------
Benoît Rousseau : roussebe at spray dot se
Jouez en programmant : http://realtimebattle.sourceforge.net/



Avatar
Marc Boyer
Hugues Delorme wrote:
Pourtant, ceci pourrait être très pratique pour
définir un schéma d'initialisation commun à tous les descendants(une
sorte


de patron de méthode pour la création d'instances) :


Ben, n'est-ce pas le rôle de l'appel à A::A() ?


Bien sûr, mais pouvoir appeler des méthodes retardées dans le constructeur
de la classe ancêtre permettrait d'avoir une initialisation plus générique.
Dans l'exemple que j'ai donné, définir une méthode do_initialize() retardée
ne semble pas justifié vu la simplicité du code. Mais pour des classes plus
complexes, l'utilité me semble très intéressante.
Un autre exemple pour tenter d'illustrer le propos :

class B
{
public:
B (list<int>& ints)//patron de méthode
{
list<int>::iterator i = ints.begin ();
while (i != ints.end ())
{
do_append (*i);//appel primitive
i++;
}
}
virtual void do_append (int i) = 0;//primitive ajout
virtual void do_prune (int index) = 0;//primitive suppression
...
};

class BImpl : public B
{
public:
BImpl (list<int>& ints) : B (ints) { }
void do_append (int i)//implémentation de la primitive
{
ints.push_back (i);
}
private:
vector<int> ints;
};

Dans cet exemple, la classe B laisse à ses descendants le choix du type de
conteneur(vector, list, map, etc.) pour copier 'ints' qu'elle reçoit en
paramètre. B utilise les méthode retardées 'do_(...)' pour manipuler le
conteneur réel.
Mais ce genre de collaboration(issu du pattern patron de méthode) entre
ancêtre et descendants est impossible en C++ (autres langages je ne sais
pas), si le patron est implémenté dans un constructeur.


En général, quand je pense patron, la traduction C++ c'est plutôt
template qu'héritage. Le problème est pas abordé dans le "Design
Pattern" ?
Ceci dit, dans ce cas, j'ai l'impression que BImpl hérite de
B pour faire conjointement de l'héritage d'interface et de
l'héritage d'implémentation, non ?

Le problème est bien celui-ci : il est impossible d'appeler des méthodes
virtuelles pures d'une classe dans le code de ses constructeurs.
En considérant le code ci-dessus, je suis obligé d'effectuer la copie de
'ints' dans le constructeur de BImpl :


J'ai une solution sans recopie de code: elle est un peu
lourde, mais ça marche. Elle utilise:
BInterface: interface abstraite de B
BStorage: implementation abstraite du stockage des éléments de B
BDefImpl: implementation par defaut de B, contient un BStorage&
BImpl: une implémentation particulière de B, elle a une sous-classe
qui implémente BStorage

Ca répond au problème général, peut-être qu'on peut simplifier.

#include <list>
#include <vector>

using std::list;

class BStorage {
public:
void fill(list<int>& ints){
list<int>::iterator i = ints.begin ();
while (i != ints.end () ) {
do_append (*i);//appel primitive
i++;
}
}
virtual void do_append (int i) = 0;
virtual ~BStorage() = 0;
};


class BInterface {
public:
BInterface (list<int>& ints);
virtual ~BInterface() = 0;
};

class BDefImpl {
BStorage& cont;
public:
BDefImpl(list<int>& ints, BStorage& theContainer):cont(theContainer){
cont.fill(ints);
}
};

class BImpl: public BInterface, private BDefImpl {
class BStorageImpl: public BStorage {
void do_append (int i){ vints.push_back(i); }
virtual ~BStorageImpl(){};
private:
std::vector<int> vints;
};
public:
BImpl(list<int>& ints): BInterface(ints),
BDefImpl(ints, *(new BStorageImpl)){};
virtual ~BImpl(){};
};


Marc Boyer
--
Lying for having sex or lying for making war? Trust US presidents :-(



Avatar
Hugues Delorme
En général, quand je pense patron, la traduction C++ c'est plutôt
template qu'héritage. Le problème est pas abordé dans le "Design
Pattern" ?
Ceci dit, dans ce cas, j'ai l'impression que BImpl hérite de
B pour faire conjointement de l'héritage d'interface et de
l'héritage d'implémentation, non ?


Une description du pattern :
http://www-sop.inria.fr/axis/cbrtools/manual/DesignPatterns/patronMethodes.s
html
Plutôt template qu'héritage? Peut-être qu'il est possible de faire une
version générique du pattern "template method", mais la forme utilisant
l'héritage me convient suffisamment.
L'héritage ne semble pas être un héritage d'implémentation puisque dans
l'exemple, la classe ancêtre(B) est retardée. D'après la classification des
héritages de B.Meyer, apparemment l'héritage est un héritage de
concrétisation ici.

J'ai une solution sans recopie de code: elle est un peu
lourde, mais ça marche. Elle utilise:
BInterface: interface abstraite de B


Une interface est une classe dont toutes les méthodes sont abstraites. Donc
une interface est toujours 'abstraite' normalement.

BStorage: implementation abstraite du stockage des éléments de B


Une implémentation abstraite?

BDefImpl: implementation par defaut de B, contient un BStorage&
BImpl: une implémentation particulière de B, elle a une sous-classe
qui implémente BStorage

Ca répond au problème général, peut-être qu'on peut simplifier.

#include <list>
#include <vector>

using std::list;

class BStorage {
public:
void fill(list<int>& ints){
list<int>::iterator i = ints.begin ();
while (i != ints.end () ) {
do_append (*i);//appel primitive
i++;
}
}
virtual void do_append (int i) = 0;
virtual ~BStorage() = 0;
};


class BInterface {
public:
BInterface (list<int>& ints);
virtual ~BInterface() = 0;
};

class BDefImpl {
BStorage& cont;
public:
BDefImpl(list<int>& ints, BStorage& theContainer):cont(theContainer){
cont.fill(ints);
}
};

class BImpl: public BInterface, private BDefImpl {
class BStorageImpl: public BStorage {
void do_append (int i){ vints.push_back(i); }
virtual ~BStorageImpl(){};
private:
std::vector<int> vints;
};
public:
BImpl(list<int>& ints): BInterface(ints),
BDefImpl(ints, *(new BStorageImpl)){};
virtual ~BImpl(){};
};



Merci pour la solution, mais j'ai peur qu'elle ne complexifie les choses.
J'ai d'abord essayé une approche qui me semblait fonctionner, mais au final
non(si quelqu'un peut me dire pourquoi) :

class B
{
public:
B (list<int>& ints, B& b)//l'astuce est ici, la primitive sera appelée
pour b.
{
list<int>::iterator i = ints.begin ();
while (i != ints.end ())
{
b.do_append (*i);//do_append est appelée sur b.
i++;
}
}
virtual void do_append (int i) = 0;
};

class BImpl : public B
{
public:
BImpl (list<int>& ints) : B (ints, *this)//On passe *this au constructeur
de l'ancêtre
{
}
void do_append (int i)
{
ints.push_back (i);
}
private:
vector<int> ints;
};

Ce code compile sans problème, mais on obtient une erreur à l'exécution :
"pure virtual method called". Pourtant le polymorphisme devrait fonctionner
ici (appeler la version de BImpl et pas celle de B pour do_append).

Pour l'instant, j'ai opté pour un compromis acceptable entre duplication et
factorisation :

class B
{
public:
B (list<int>& ints) {}

void initialize (list<int>& ints)//le code du constructeur est ici
{
list<int>::iterator i = ints.begin ();
while (i != ints.end ())
{
do_append (*i);
i++;
}
}
virtual void do_append (int i) = 0;//primitive ajout
};

class BImpl : public B
{
public:
BImpl (list<int>& ints) : B (ints)
{
initialize (ints);//appel nécessaire à initialize
}
void do_append (int i)
{
ints.push_back (i);
}
private:
vector<int> ints;
};


Marc Boyer
--
Lying for having sex or lying for making war? Trust US presidents :-(


Avatar
Jean-Marc Bourguet
"Hugues Delorme" writes:

Merci pour la solution, mais j'ai peur qu'elle ne complexifie les choses.
J'ai d'abord essayé une approche qui me semblait fonctionner, mais au final
non(si quelqu'un peut me dire pourquoi) :


A nouveau durant la construction, tout appel a une fonction virtuelle
reference l'objet construit.

class B
{
public:
B (list<int>& ints, B& b)//l'astuce est ici, la primitive sera appelée
pour b.


comme b == this, on est pendant la construction de b donc c'est les
fonctions virtuelles de B qui sont referencees.

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org

Avatar
Benoit Rousseau
Hugues Delorme wrote:

J'ai d'abord essayé une approche qui me semblait fonctionner, mais au final
non(si quelqu'un peut me dire pourquoi) :

class B
{
public:
B (list<int>& ints, B& b)//l'astuce est ici, la primitive sera appelée
pour b.
{
list<int>::iterator i = ints.begin ();
while (i != ints.end ())
{
b.do_append (*i);//do_append est appelée sur b.
i++;
}
}
virtual void do_append (int i) = 0;
};

class BImpl : public B
{
public:
BImpl (list<int>& ints) : B (ints, *this)//On passe *this au constructeur
de l'ancêtre
{
}
void do_append (int i)
{
ints.push_back (i);
}
private:
vector<int> ints;
};

Ce code compile sans problème, mais on obtient une erreur à l'exécution :
"pure virtual method called". Pourtant le polymorphisme devrait fonctionner
ici (appeler la version de BImpl et pas celle de B pour do_append).


Parceque (*this) n'est pas encore de type BImpl quand tu appelles
B(ints, *this). La fonction appelée dans B::B(...) est B::do_append et
non pas BImpl::do_append. On se retrouve quasiment dans le même cas qu'à
la question à l'origine de ce thread.


Pour l'instant, j'ai opté pour un compromis acceptable entre duplication et
factorisation :

class B
{
public:
B (list<int>& ints) {}

void initialize (list<int>& ints)//le code du constructeur est ici
{
list<int>::iterator i = ints.begin ();
while (i != ints.end ())
{
do_append (*i);
i++;
}
}
virtual void do_append (int i) = 0;//primitive ajout
};


Ici, l'appel de B::initialize se fait bien lorsque BImpl est "complet",
et non pas lorsqu'il est en cours de construction, puisqu'il est fait
dans BIntl::BIntl().

Toujours avec le même exemple de A et B (qui ne doit pas valoir grand
chose, mais bon) avec gcc :

class A {
public:
A() {
cout<< typeid( *this ).name() <<endl;
}

virtual void t() {
cout << typeid( *this ).name()<<endl;
}
void r() {
cout << typeid( *this ).name()<<endl;
}
};

class B : public A {
public:
B() : A() {};
};

A la construction d'un B, A::A() affiche le type de A
alors que l'appel de t() et de r() affichent bien le type de B.



--
--------------------------------------------
Benoît Rousseau : roussebe at spray dot se
Jouez en programmant : http://realtimebattle.sourceforge.net/

1 2