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.

2 réponses

1 2
Avatar
Marc Boyer
In article <bp04q3$42l$, Hugues Delorme wrote:
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.


Toute la difficulté vient du fait que tu cherches à l'implémenter
*dans le constructeur*. A tout autre endroit, ce serait simplissime.

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.


Ouaip.
Ceci dit, le code doit il pouvoir manipuler différentes concrétisations
de façon dynamique ou non ? Si la réponse est non, je serais tenté de
faire un template avec une instantiation.


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.


Ouaip.

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


Une implémentation abstraite?


Implémentation par défaut, avec au moins une méthode abstraite.

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


Ca fait pas ce que tu veux puisque, lors de la construction de
B, *this est encore un B. Il ne sera un BImpl que plus tard.

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


Non, cf ce dessus.

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


Oui, ça me semble un bon compromis. Tout dépend du risque
d'oublier le "initialize".

En fait, de façon générale,
si on veut utiliser le polymorphisme dans un constructeur, il faut
que l'objet polymorphique soit crée "avant", donc, qu'il ne soit
pas l'objet lui même qu'on construit.

L'objet polymorphique, c'est celui qui fait les "do_append",
et la réalisation de B (BImpl) doit la passer au constructeur de
B. On a le même principe si on fait un template.

On peut donc un rien simplifier ma solution:

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 AbstractB {
BStorage& cont;
public:
AbstractB(list<int>& ints, BStorage& theContainer):cont(theContainer){
cont.fill(ints);
}
virtual ~AbstractB() = 0;
};

class BImpl: private AbstractB {
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): AbstractB(ints, *(new BStorageImpl)){};
virtual ~BImpl(){};
};


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


Avatar
kanze
Marc Boyer wrote in message
news:<bovoi7$g8j$...
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...


Disons plutôt que tu n'as pas encore un objet de type AImpl.

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


Ça dépend. Disons que la risque (bien réele -- je l'ai expérimenté en
Java) qu'on cherche à éviter, c'est que do_initialize va accéder (non
forcémement modifier) des éléments de AImpl, qui évidemment n'ont pas
encore été initialisés.

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


C'est une variation sur le modèle de template, où la customisation a
lieu déjà dans le constructeur ; il sert souvent en Swing, par exemple,
quand il y a des délégués, pour établir le délégué initial. (Et si le
délégué initial dépend des données de la classe dérivée, tu es cuit.
Comme j'ai dit ci-dessus, la risque contre laquelle C++ protège est bien
réele.)

Il existe plusieurs solutions. Quand il s'agit d'établir le délégué
initial, la solution la plus simple est d'en passer l'adresse comme
paramètre du constructeur, comme dans l'hièrarchie des iostream (ou la
version de GB_FieldArray qui se trouve actuellement sur ma site).

Sinon, dans la passée, je me suis servi des types spéciaux
d'initialisation pour forcer l'appel d'une fonction membre à la fin de
l'expression où l'objet a été construit, ainsi :

class AInitializer
{
friend class A ;
public :
AInitializer()
: myOwner( NULL )
{
}
~AInitializer()
{
if ( myOwner != NULL ) {
myOwner->finishInitialization() ;
}
}
private :
A* myOwner ;
} ;

class A
{
public:
A( AInitializer const& init = AInitializer() ) ;
virtual void finishInitialization() = 0 ;
// ...
} ;

A::A( AInitializer const& init )
{
const_cast< AInitializer& >( init ).myOwner = this ;
// ...
}

Ce n'est pas, à mon avis, une solution idéale. Elle ne marche que dans
la mésure que l'initialisateur est un objet temporaire, ce qu'on ne peut
jamais réelement garantir, et même dans ce cas-là, l'initialisation
n'est finie qu'à la fin de l'expression, non à la fin de la
sous-expression A(). Si Ce n'est pas en général un problème pour des
types entité, ça peut l'être pour les autres. La version originale de
FieldArray, par exemple, utilisait le modèle de template, et non la
délégation, et utilisait cette technique pour appeler la fonction
virtuelle pûre « set » à la fin de la construction, dans le cas où on
lui initialise avec une chaîne. Jusqu'au jour où un collègue à voulu
écrire :

GB_BlankSeparatedFieldArray( line )[ 1 ]

pour accéder au premier champ, sans se soucier de reste.

--
James Kanze GABI Software mailto:
Conseils en informatique orientée objet/ http://www.gabi-soft.fr
Beratung in objektorientierter Datenverarbeitung
11 rue de Rambouillet, 78460 Chevreuse, France, +33 (0)1 30 23 45 16


1 2