Idiome pImpl – exemples

L'idiome pImpl, pour Private Implementation, apparaît à quelques endroits dans mes notes de cours (et dans mes prestations), typiquement sous un angle parmi plusieurs (celui, en fait, qui me semble le plus susceptible d'être utile à mes étudiant(e)s). Par acquis de conscience, cependant, voici un petit complément d'information.

De manière générale, un idiome comme pImpl a plusieurs acceptions, et chacune a ses avantages et ses inconvénients.

Pour un exemple très banal (système de génération de nombres entiers séquentiels), on pourrait, avec l'approche apparaissant dans vos notes de cours, avoir un truc comme celui proposé à droite.

Dans cet exemple, mis à part l'allocation dynamique de mémoire dans GenSeq::creer() et sa contrepartie dans GenSeq::liberer(), le coût de l'approche tient au passage à travers le service prochain() de l'interface IGenSeq, ce qui constitue une invocation polymorphique, donc un appel indirect – à travers un pointeur de fonction – qui ne peut pas, règle générale, bénéficier du « inlining ».

L'idiome en soi est « gratuit », ce qui explique son intérêt.

Dans le .cpp à droite, si nous avions voulu réutiliser un seul GenerateurFullGenial pour tous le clients plutôt que de les instancier à la pièce, il aurait suffi de remplacer les méthodes GenSeq::creer() et GenSeq::liberer() proposées à droite par quelque chose comme ce qui suit :

namespace
{
   GenerateurFullCool gfc; // global invisible à l'édition des liens
}
IGenSeq *GenSeq::creer()
    { return &::gfc; } // rien à construire; l'objet existe déjà
void GenSeq::liberer(const IGenSeq *) noexcept
    {  } // rien de construit, ergo, rien à détruire!

Bien entendu, si un seul générateur est utilisé, cela change la sémantique du programme, alors le choix d'approche doit d'abord et avant tout se conformer aux besoins de l'application...

Fichier GenSeq.h
#ifndef GEN_SEQ_H
#define GEN_SEQ_H
struct IGenSeq
{
    using value_type = int;
    virtual value_type prochain() = 0;
protected:
    virtual ~IGenSeq() = default;
    friend class GenSeq;
};
#include "Incopiable.h" // je suppose que c'est évident
class GenSeq
    : Incopiable
{
    IGenSeq *p_;
    static IGenSeq *creer();
    static void liberer(const IGenSeq *) noexcept;
public:
    GenSeq()
       : p_{creer()}
    {
    }
    ~GenSeq() noexcept
       { liberer(p_); }
    using value_type = IGenSeq::value_type;
    value_type prochain()
       { return p_->prochain(); }
};
#endif
Fichier GenSeq.cpp
#include "GenSeq.h"
class GenerateurFullGenial
    : public IGenSeq
{
    value_type cur_;
public:
    GenerateurFullGenial(const value_type &init = {})
       : cur_{init}
    {
    }
    value_type prochain()
       { return cur_++; }
};
IGenSeq *GenSeq::creer()
    { return new GenerateurFullGenial; }
void GenSeq::liberer(const IGenSeq *p) noexcept
    { delete p; }
Programme de test
#include "GenSeq.h"
#include <iostream>
int main()
{
    using namespace std;
    GenSeq gen;
    for (int i = 0; i < 10; ++i)
       cout << gen.prochain() << endl;
}

On peut aussi dissimuler complètement l'implémentation si on le souhaite, comme on me l'a fait remarquer (j'attribuais la manoeuvre à Sutter, mais on m'a gentiment rappelé que Lakos l'a proposé bien avant).

Ça donnerait quelque chose comme ceci (à droite).

Dans GenSeq, on peut utiliser un GenSeq::Impl* (un pointeur) dans la mesure où on ne manipule rien qui ait trait à son contenu ou à sa nature (pas d'appel de méthode, incluant le destructeur, tant qu'on n'a pas défini ce que cette classe a comme forme).

Jusqu'à preuve du contraire, le type GenSeq::Impl est dit incomplet et ne peut être manipulé en propre, autrement que par des indirections et, même alors, seulement en tant qu'indirection.

Cette dernière version, donc, perd l'opportunité du inlining sur GenSeq::prochain(), mais évite, en contrepartie, le polymorphisme lors de l'appel à GenSeq::Impl::prochain(). Notez que, si on souhaite retrouver la possibilité d'avoir plusieurs implémentations distinctes, alors on devra réintroduire la strate polymorphique.

Le plus gros avantage de cette version est que le .h n'expose vraiment rien de l'implémentation, donc c'est très agréable pour l'entretien du code.

Fichier GenSeq.h
#ifndef GEN_SEQ_H
#define GEN_SEQ_H
#include "Incopiable.h" // je suppose que c'est évident
class GenSeq
    : Incopiable
{
    class Impl; // on introduit le nom sans rien dire d'autre
    Impl *p_;
    static Impl *creer();
    static void liberer(const Impl *) noexcept;
public:
    GenSeq()
       : p_{creer()}
    {
    }
    ~GenSeq() noexcept
       { liberer(p_); }
    using value_type = int;
    value_type prochain();
};
#endif
Fichier GenSeq.cpp
#include "GenSeq.h"
class GenSeq::Impl
{
public:
    using value_type = GenSeq::value_type;
private:
    value_type cur_;
public:
    Impl(const value_type &init = {})
       : cur_{init}
    {
    }
    value_type prochain()
       { return cur_++; }
};
auto GenSeq::creer() -> Impl*
    { return new GenSeq::Impl; }
void GenSeq::liberer(const Impl *p)
    { delete p; }
auto GenSeq::prochain() -> value_type
    { return p_->prochain(); }
Programme de test
#include "GenSeq.h"
#include <iostream>
int main()
{
    using namespace std;
    GenSeq gen;
    for (int i = 0; i < 10; ++i)
       cout << gen.prochain() << endl;
}

Voilà!


Valid XHTML 1.0 Transitional

CSS Valide !