I. Révisions▲
Version Boost |
Révision article |
---|---|
1.36 |
Rédaction |
1.37 |
Pas de modification |
1.38 |
Pas de modification |
1.39 |
Pas de modification |
II. Avant-propos▲
Boost se veut un ensemble de bibliothèques portables destinées à faciliter la vie du développeur C++. Boost se propose de construire du code de référence pouvant aussi, à terme, être incorporé au standard (STL). Pour plus d'informations, vous pouvez consulter :
- les différents tutoriels Boost de développez.com : Tutoriels ;
- la F.A.Q Boost de développez.com : F.A.Q. ;
- le site officiel de Boost : Boost ;
- le site officiel de la bibliothèque : In Place Factories.
III. Remerciements▲
Je remercie Alp pour ses encouragements, farscape, Florian Goo, khayyam90 et RideKick pour leur(s) courageuse(s) relecture(s) et fais allégeance à hiko-seijuro mon parrain…
Enfin, d'une façon plus globale, je remercie les membres des forums de développez.com qui par la qualité de leurs interventions m'ouvrent constamment de nouvelles pistes de réflexion sur ma pratique de développement.
IV. Introduction▲
IV-A. Prérequis▲
Avant toute chose, à l'instar de l'ensemble des bibliothèques Boost, le lecteur doit être familier avec le langage C++ et plus particulièrement se sentir à l'aise avec les templates. La bibliothèque In Place Factory utilise les bases de la programmation template. Il ne faut pas que celles-ci puissent être un obstacle à leur compréhension.
L'utilisation de la bibliothèque In Place Factory ne nécessite pas de connaissances particulières sur les autres bibliothèques Boost. L'implémentation, quant à elle, s'appuie sur Boost.Preprocessor. Sa compréhension nécessite donc d'avoir déjà côtoyé cette dernière.
IV-B. Objectifs▲
Cette bibliothèque va permettre de masquer les paramètres de construction d'un objet à l'instance chargée de construire cet objet. Par exemple, supposons qu'une classe A ait besoin de construire un objet d'une classe B. Mais elle ne souhaite pas exposer dans son interface les paramètres nécessaires à la construction de B. In Place Factory va permettre d'atteindre ce but. Passons à un exemple concret pour plus de clarté :
class
B
{
public
:
B(int
_p0, double
_p1, std::
string _p2){}
}
;
class
A
{
public
:
A();
private
:
B*
m_pObjetB;
}
;
Juste un petit préambule pour préciser que ceci n'a vocation à être qu'un exemple pour la bibliothèque In Place Factory. En ce sens, une classe comme A devrait être plus complète pour gérer correctement un membre pointeur (et, plus judicieusement, utiliser les pointeurs intelligents). Le lecteur est supposé être familier avec ces notions, c'est pourquoi l'exemple, pour plus de clarté, omet volontairement ces aspects.
Pour que A puisse créer son objet de type B, nous avons les deux solutions suivantes :
void
A::
CreerB(const
B&
_copie_de_b)
{
m_pObjetB =
new
B(_copie_de_b);
}
void
A::
CreerB(int
_p0, double
_p1, std::
string _p2)
{
m_pObjetB =
new
B(_p0, _p1, _p2);
}
- Utiliser une variable temporaire de type B en supposant que B est constructible par copie.
- Exposer l'interface du constructeur de B.
La première solution présente deux inconvénients majeurs : B doit être constructible par copie et une instance temporaire est utilisée pour cette construction. La seconde solution se passe de commentaire : en effet, la maintenance de ce genre de code devient rapidement laborieuse. Comment faire ?
Boost.In Place Factory va nous permettre de résoudre ce problème. Les lecteurs familiers avec la langue de Shakespeare auront déjà traduit le nom de la bibliothèque : usine sur place ! L'idée que sous-tend cette bibliothèque est de permettre la construction (usine) de B dans A (sur place) avec tout les paramètres nécessaires sans pour autant les fournir explicitement à la méthode A::CreerB.
V. Utilisation▲
V-A. En-têtes▲
Commençons par l'essentiel : les fichiers d'en-têtes nécessaires ! Ils se limitent aux deux selon le type souhaité :
- boost/utility/in_place_factory.hpp ;
- boost/utility/typed_in_place_factory.hpp
La bibliothèque est composée de deux familles de classes que nous allons appeler in_place_factory et typed_in_place_factory. Le premier fichier d'en-tête permet d'accéder à la première famille de classe, et le second … à la seconde. Les différences entre ces deux seront étudiées ci-dessous.
V-B. Espace de nommage▲
L'espace de nommage est :
namespace
boost
V-C. In Place Factory▲
Revenons à notre problème de départ. Nous avons donc les deux classes suivantes :
class
B
{
public
:
B(int
_p0, double
_p1, std::
string _p2){}
}
;
class
A
{
public
:
A();
private
:
B*
m_pObjetB;
}
;
Nous souhaitons adjoindre une méthode à la classe A pour créer l'instance B. Deux questions se posent : quels sont les paramètres de notre méthode et comment celle-ci sera définie. Soit, en reformulant, comment cela se passe du côté du conteneur (implémentation de notre méthode A::CreerB) et comment cela se passe pour un utilisateur de la classe A (les paramètres).
Du côté du conteneur, le problème des paramètres est éludé par l'utilisation d'un template :
template
<
class
InPlaceFactoryT>
void
A::
CreerB(InPlaceFactoryT const
&
_Usine)
{
m_pObjetB =
reinterpret_cast
<
B*>
(new
char
[sizeof
(B)]);
_Usine.template
apply<
B>
(m_pObjetB);
}
La première ligne est une allocation de la taille nécessaire à un objet de type B. new char[sizeof(B)] se contente d'allouer un bloc mémoire de taille suffisante, l'adresse est ensuite transtypée en B* sans aucune vérification. L'objectif est bien de séparer l'allocation de B de l'appel au constructeur.
Le second paramètre fait appel à une méthode template de notre paramètre : InPlaceFactoryT::apply<B>. Cette méthode va appeler le constructeur de B avec l'ensemble des paramètres.
Maintenant, comment appeler cette fonction et quel est ce mystérieux paramètre InPlaceFactoryT ? L'interface apportée par Boost est encore là très simple :
À a;
a.CreerB(boost::
in_place(1
,3.14
,"une chaine"
));
Comme le montre cet exemple, la fonction boost::in_place permet de masquer la classe effectivement créée. Cela reste du ressort de la bibliothèque. Pour son utilisation, il n'est pas nécessaire d'en connaître plus. Nous reviendrons dessus dans les détails d'implémentation un peu plus loin.
V-D. Les tableaux▲
En théorie, il est possible d'utiliser in_place_factory pour un tableau d'éléments. Une version surchargée prend le nombre d'éléments contenus dans le tableau :
template
<
class
InPlaceFactoryT>
void
A::
Creer10B(InPlaceFactoryT const
&
_Usine)
{
m_pObjetB =
reinterpret_cast
<
B*>
(new
char
[sizeof
(B)*
10
]);
_Usine.template
apply<
B>
(m_pObjetB,10
);
}
Les mêmes paramètres sont utilisés pour la construction de tous les éléments.
En théorie ? Oui, car ce code ne compile pas à cause de l'implémentation interne de la fonction apply. Dans la section précisant les détails d'implémentation, un contournement est proposé.
V-E. Typed In Place Factory▲
La différence entre Typed In Place Factory et In Place Factory tient, comme le nom l'indique, dans la précision du nom du type auquel s'applique la construction. Dans la version In Place Factory, le type de l'objet est indiqué du côté du conteneur au moment de l'appel à apply :
template
<
class
InPlaceFactoryT>
void
A::
CreerB(InPlaceFactoryT const
&
_Usine)
{
m_pObjetB =
reinterpret_cast
<
B*>
(new
char
[sizeof
(B)]);
_Usine.template
apply<
B>
(m_pObjetB);
}
Dans la version Typed In Place Factory, le type est déclaré par l'utilisateur :
À a;
a.CreerB(boost::
in_place<
B>
(1
,3.14
,"une chaine"
));
Côté conteneur, cela devient tout simplement :
template
<
class
InPlaceFactoryT>
void
A::
CreerB(InPlaceFactoryT const
&
_Usine)
{
m_pObjetB =
reinterpret_cast
<
B*>
(new
char
[sizeof
(B)]);
_Usine.apply(m_pObjetB);
}
Quel est l'intérêt de Typed In Place Factory ? Lorsque la classe A n'a pas connaissance du type exact de B (par exemple, dans le cas où du polymorphisme intervient), cette connaissance est déléguée à l'appelant. L'exemple suivant illustre ce cas :
class
BBase
{
}
;
class
B : public
BBase
{
public
:
B(int
_p0, double
_p1, std::
string _p2);
~
B();
}
;
class
B2 : public
BBase
{
public
:
B2();
~
B2();
}
;
class
A
{
public
:
A();
~
A();
template
<
class
TypedInPlaceFactoryB>
void
CreerBBase(BBase*
_pAdresse, TypedInPlaceFactoryB const
&
_Usine)
{
m_pObjetBase =
_pAdresse;
_Usine.apply(m_pObjetBase);
}
protected
:
BBase *
m_pObjetBase;
}
;
int
main()
{
À a;
char
un_b[sizeof
(B)];
a.CreerBBase(
reinterpret_cast
<
BBase*>
(&
un_b)
,boost::
in_place<
B>
(1
,3.14
,"une chaine"
)// On veut un B
);
char
un_b2[sizeof
(B2)];
a.CreerBBase(
reinterpret_cast
<
BBase*>
(&
un_b2)
,boost::
in_place<
B2>
()// On veut un B2
);
return
0
;
}
V-F. Contraintes▲
V-F-1. L'espace de stockage▲
L'entité en charge de créer l'instance de l'élément qui va être construit doit correctement gérer son espace de stockage, notamment assurer une taille suffisante, veiller aux problèmes d'alignement, mettre en cohérence son allocation et sa libération si besoin. L'utilisation de Boost.Optional permet de contourner ces contraintes. Elles sont directement gérées par ce module. Pour plus d'informations, vous pouvez consulter l'aide sur la bibliothèque Boost.Optional.
V-F-2. Limites en nombre d'arguments▲
Le nombre maximal de paramètres que l'on peut ainsi passer au constructeur est de BOOST_MAX_INPLACE_FACTORY_ARITY. Cette valeur est actuellement positionnée à 10. C'est largement suffisant dans la plupart des cas. Dans la section dédiée à l'implémentation, il est indiqué comment modifier cette valeur si on a besoin de paramètres supplémentaires.
V-F-3. Durée de vie des arguments▲
L'utilisation habituelle consiste à donner le résultat de l'appel à in_place comme argument à l'entité chargée de construire in-fine l'instance via la méthode apply. Cependant, si ces deux appels sont décalés, il faut veiller à ce que les arguments positionnés dans la méthode in_place soient encore valides lorsque l'entité appelle la méthode apply.
V-G. En résumé▲
In Place Factory |
Typed In Place Factory |
|
---|---|---|
en-têtes |
<boost/utility/in_place_factory.hpp> |
<boost/utility/typed_in_place_factory.hpp> |
côté conteneur |
template<class InPlaceFactoryT>… : |
template<class TypedInPlaceFactoryT>… : |
côté utilisateur |
boost::in_place(P0,P1,…,P9) |
boost::in_place<ClasseT>(P0,P1,…,P9) |
VI. Comment ça marche ?▲
Les deux éléments constitutifs de la bibliothèque sont la famille de classes associées et la fonction in_place.
VI-A. Les classes in_place_factory▲
La bibliothèque définit une classe selon le nombre de paramètres du constructeur. Ces classes sont :
- in_place_factory0 pour un constructeur sans paramètre ;
- in_place_factory1 pour un constructeur avec 1 paramètre ;
- in_place_factory2 pour un constructeur avec 2 paramètres ;
- … ;
- in_place_factoryN pour un constructeur avec N paramètres.
Chacune des classes suit le schéma suivant :
class
in_place_factory_base {}
;
template
<
class
A1, class
A2, ..., class
AN>
class
in_place_factoryN
:
public
in_place_factory_base
{
public
:
explicit
in_place_factoryN
( A1 const
&
a1, A2 const
&
a2,..., AN const
&
aN)
:
m_a1(a1), m_a2(a2),...,m_aN(aN)
{}
template
<
class
T>
void
*
apply(void
*
address) const
{
return
new
(address) T( m_a1, m_a2,...,m_aN );
}
template
<
class
T>
void
*
apply(void
*
address, std::
size_t n) const
{
for
(char
*
next =
address =
this
->
template
apply<
T>
(address);
!!
--
n;)
this
->
template
apply<
T>
(next =
next+
sizeof
(T));
return
address;
}
A1 const
&
m_a1;
A2 const
&
m_a2;
...
AN const
&
m_aN;
}
;
VI-B. La fonction in_place▲
À l'instar des familles de classes, on retrouve une famille de fonctions in_place surchargées suivant le nombre d'arguments :
inline
in_place_factory0 in_place(){
return
in_place_factory0();
}
template
<
class
A1>
inline
in_place_factory1 in_place(A1 const
&
a1){
return
in_place_factory1<
A1>
(a1);
}
template
<
class
A1, class
A2>
inline
in_place_factory1 in_place(A1 const
&
a1, A2 const
&
a2){
return
in_place_factory2<
A1,A2>
(a1, a2);
}
...
template
<
class
A1, class
A2, ... class
AN>
inline
in_place_factoryn in_place(A1 const
&
a1, A2 const
&
a2, ..., AN const
&
aN){
return
in_place_factoryN<
A1,A2,...,AN>
(a1, a2,...,aN);
}
Chacune de ces fonctions crée une instance de la classe adéquate et la retourne. Pourquoi passer par de telles fonctions ? Tout simplement pour avoir une interface uniforme qui va masquer à l'utilisateur le détail des différentes classes existantes. En effet, le compilateur sait quelle surcharge choisir grâce au nombre d'arguments.
VI-C. Et pour typed_in_place_factory▲
Les classes typed_in_place_factory se différencient des classes in_place_factory sur la remontée du template de la classe à construire, de la méthode apply vers la classe typed_in_place_factory elle-même. On retrouve donc une implémentation similaire :
class
typed_in_place_factory_base {}
;
template
<
class
T, class
A1, class
A2, ..., class
AN >
class
typed_in_place_factoryN
:
public
typed_in_place_factory_base
{
public
:
typedef
T value_type;
explicit
typed_in_place_factoryN
( A1 const
&
a1, A2 const
&
a2,...,AN const
&
aN)
:
m_a1(a1),m_a2(a2),...m_aN(aN)
{}
void
*
apply (void
*
address) const
{
return
new
(address) T( m_a1, m_a2,...,m_aN );
}
void
*
apply (void
*
address, std::
size_t n) const
{
for
(void
*
next =
address =
this
->
apply(address); !!
--
n;)
this
->
apply(next =
static_cast
<
char
*>
(next) +
sizeof
(T));
return
address;
}
A1 const
&
m_a1;
A2 const
&
m_a2;
...
AN const
&
m_aN;
}
;
template
<
class
T, class
A1, class
A2,..., class
AN>
inline
typed_in_place_factoryN<
T, A1, A2,..., AN>
in_place( A1 const
&
a1,A2 const
&
a2,...,AN const
&
aN )
{
return
typed_in_place_factoryN<
T, A1, A2,..., AN >
( a1, a2,..., aN);
}
VI-D. Génération des classes/fonctions▲
VI-D-1. Boost.Preprocessor▲
Tout bon développeur qui se respecte a une sainte horreur du copier/coller. Ces listes de fonctions et de classes n'ont effectivement pas été générées ainsi. La bibliothèque utilise Boost.Preprocessor pour générer chacune des instances. Elle s'appuie sur l'itération par inclusion des fichiers en-tête.
Voici, par exemple, comment sont définies les fonctions :
#if N > 0
template
<
BOOST_PP_ENUM_PARAMS(N, class
A) >
inline
BOOST_PP_CAT(in_place_factory,N)<
BOOST_PP_ENUM_PARAMS(N, A) >
in_place( BOOST_PP_ENUM_BINARY_PARAMS(N, A, const
&
a) )
{
return
BOOST_PP_CAT(in_place_factory,N)<
BOOST_PP_ENUM_PARAMS(N, A) >
( BOOST_PP_ENUM_PARAMS(N, a) );
}
#else
inline
in_place_factory0 in_place()
{
return
in_place_factory0();
}
#endif
Le mécanisme d'itération par inclusion des fichiers d'en-tête permet d'inclure ce bout de code de 0 à MAX fois. N est le compteur d'itération. Une rapide explication des différentes macros :
- BOOST_PP_ENUM_PARAMS(N, class A) : génère la liste class A1, class A2… class AN ;
- BOOST_PP_CAT(in_place_factory,N) : concatène la valeur des deux arguments : soit si N vaut 5, on obtient in_place_factory5 ;
- BOOST_PP_ENUM_PARAMS(N, A) : génère la liste A1, A2…, AN ;
- BOOST_PP_ENUM_BINARY_PARAMS(N, A, const& a) : génère la liste A1 const&a1, A2 const&a2,… AN const&aN ;
Pour plus d'informations, vous pouvez consulter l'aide sur la bibliothèque Boost.Preprocesssor.
VI-D-2. Limite du nombre d'arguments▲
Le nombre d'itérations est positionné par la macro-constante BOOST_MAX_INPLACE_FACTORY_ARITY dans le fichier boost/utility/detail/in_place_factory_prefix.hpp. Pour augmenter le nombre de paramètres gérés, il suffit de modifier cette macro. À noter que cette valeur ne peut excéder 255, limite inhérente à Boost.Preprocessor.
VI-E. Et pour mes tableaux▲
Précédemment, on a laissé entendre que l'on pouvait utiliser l'initialisation sur place aussi pour un tableau :
template
<
class
InPlaceFactoryT>
void
A::
Creer10B(InPlaceFactoryT const
&
_Usine)
{
m_pObjetB =
reinterpret_cast
<
B*>
(new
char
[sizeof
(B)*
10
]);
_Usine.template
apply<
B>
(m_pObjetB,10
);
}
Cependant, si vous compilez ce code, vous obtiendrez probablement une erreur du type :
in_place_factory.hpp:61
: error: invalid conversion from `void
*
' to `char
*
'
En effet, si on reprend la définition de la classe :
template
<
class
T>
void
*
apply(void
*
address, std::
size_t n) const
{
for
(char
*
next =
address =
this
->
template
apply<
T>
(address);
!!
--
n;)
this
->
template
apply<
T>
(next =
next+
sizeof
(T));
return
address;
}
À noter que la définition des classes typed_in_place_factory ne pose aucun souci de compilation :
void
*
apply (void
*
address, std::
size_t n) const
{
for
(void
*
next =
address =
this
->
apply(address); !!
--
n;)
this
->
apply(next =
static_cast
<
char
*>
(next) +
sizeof
(T));
return
address;
}
Il ne nous reste donc qu'à modifier les classes in_place_factory en ce sens.
template
<
class
T>
void
*
apply(void
*
address, std::
size_t n) const
{
for
(void
*
next =
address =
this
->
template
apply<
T>
(address);
!!
--
n;)
this
->
template
apply<
T>
(next =
static_cast
<
char
*>
(next) +
sizeof
(T));
return
address;
}
Voilà l'illustration parfaite des erreurs générées par un copier/coller de code…