Tutoriel Boost In Place Factory

3DArchi

Cet article présente la bibliothèque In Place Factory/Typed In Place Factory de la famille de bibliothèques Boost.

- Titre : Tutoriel Boost In Place Factory

- Auteur : 3DArchi

- Parution : 22 décembre 2008

- Mise à jour : 26 juin 2020

- Licence : Copyright ® 2008 3DArchi. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Révisions

- II. Avant-propos

- III. Remerciements

- IV. Introduction

- IV-A. Prérequis

- IV-B. Objectifs

- V. Utilisation

- V-A. En-têtes

- V-B. Espace de nommage

- V-C. In Place Factory

- V-D. Les tableaux

- V-E. Typed In Place Factory

- V-F. Contraintes

- V-F-1. L'espace de stockage

- V-F-2. Limites en nombre d'arguments

- V-F-3. Durée de vie des arguments

- V-G. En résumé

- VI. Comment ça marche ?

- VI-A. Les classes in_place_factory

- VI-B. La fonction in_place

- VI-C. Et pour typed_in_place_factory

- VI-D. Génération des classes/fonctions

- VI-D-1. Boost.Preprocessor

- VI-D-2. Limite du nombre d'arguments

- VI-E. Et pour mes tableaux

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é :

exemple :

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 :

Copie par construction :

void A::CreerB(const B& _copie_de_b)
{
m_pObjetB = new B(_copie_de_b);
}

Copie par exposition de l'interface :

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 :

exemple :

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 :

Côté conteneur :

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 :

Côté utilisateur :

À 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 :

Utilisation sur un tableau d'éléments :

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 :

Côté conteneur :

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 :

typed_in_place_factory, côté utilisateur :

À a;
a.CreerB(boost::in_place<B>(1,3.14,"une chaine"));

Côté conteneur, cela devient tout simplement :

typed_in_place_factory, Côté conteneur :

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 :

typed_in_place_factory :

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>… :
_Usine.template apply<ClasseT>(_pAdresse);

template<class TypedInPlaceFactoryT>… :
_Usine.apply(_pAdresse);

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 :

classe in_place_factoryN :

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 :

Fonctions in_place :

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 :

typed_in_place_factory :

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 :

Utilisation sur un tableau d'éléments :

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 :

erreur :

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.

Modification :

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…