Namespaces
Variants

PImpl

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

"Pointer to implementation" ou "pImpl" est une technique de programmation C++ qui supprime les détails d'implémentation d'une classe de sa représentation objet en les plaçant dans une classe séparée, accessible via un pointeur opaque :

// --------------------
// interface (widget.h)
struct widget
{
    // membres publics
private:
    struct impl; // déclaration anticipée de la classe d'implémentation
    // Exemple d'implémentation : voir ci-dessous pour d'autres options de conception et compromis
    std::experimental::propagate_const< // wrapper de pointeur à propagation de constance
        std::unique_ptr<                // pointeur opaque à propriété unique
            impl>> pImpl;               // vers la classe d'implémentation déclarée anticipée
};
// ---------------------------
// implémentation (widget.cpp)
struct widget::impl
{
    // détails d'implémentation
};

Cette technique est utilisée pour construire des interfaces de bibliothèque C++ avec une ABI stable et pour réduire les dépendances de compilation.

Table des matières

Explication

Parce que les membres de données privés d'une classe participent à sa représentation d'objet, affectant la taille et la disposition, et parce que les fonctions membres privées d'une classe participent à la résolution de surcharge (qui a lieu avant la vérification d'accès aux membres), toute modification de ces détails d'implémentation nécessite la recompilation de tous les utilisateurs de la classe.

pImpl supprime cette dépendance de compilation ; les modifications apportées à l'implémentation ne provoquent pas de recompilation. Par conséquent, si une bibliothèque utilise pImpl dans son ABI, les versions plus récentes de la bibliothèque peuvent modifier l'implémentation tout en restant compatibles ABI avec les versions antérieures.

Compromis

Les alternatives à l'idiome pImpl sont

  • implémentation inline : les membres privés et les membres publics sont membres de la même classe.
  • classe purement abstraite (usine POO) : les utilisateurs obtiennent un pointeur unique vers une classe de base légère ou abstraite, les détails d'implémentation se trouvent dans la classe dérivée qui redéfinit ses fonctions membres virtuelles.

Pare-feu de compilation

Dans les cas simples, à la fois pImpl et la méthode factory suppriment la dépendance à la compilation entre l'implémentation et les utilisateurs de l'interface de classe. La méthode factory crée une dépendance cachée sur la vtable, donc réorganiser, ajouter ou supprimer des fonctions membres virtuelles brise l'ABI. L'approche pImpl n'a pas de dépendances cachées, cependant si la classe d'implémentation est une spécialisation de template de classe, l'avantage du pare-feu de compilation est perdu : les utilisateurs de l'interface doivent observer la définition complète du template afin d'instancier la spécialisation correcte. Une approche de conception courante dans ce cas est de restructurer l'implémentation de manière à éviter la paramétrisation, ce qui constitue un autre cas d'utilisation pour les C++ Core Guidelines :

Par exemple, la classe template suivante n'utilise pas le type T dans ses membres privés ou dans le corps de push_back :

template<class T>
class ptr_vector
{
    std::vector<void*> vp;
public:
    void push_back(T* p)
    {
        vp.push_back(p);
    }
};

Par conséquent, les membres privés peuvent être transférés à l'implémentation tels quels, et push_back peut rediriger vers une implémentation qui n'utilise pas non plus T dans l'interface :

// ---------------------
// header (ptr_vector.hpp)
#include <memory>
class ptr_vector_base
{
    struct impl; // ne dépend pas de T
    std::unique_ptr<impl> pImpl;
protected:
    void push_back_fwd(void*);
    void print() const;
    // ... voir section implémentation pour les fonctions membres spéciales
public:
    ptr_vector_base();
    ~ptr_vector_base();
};
template<class T>
class ptr_vector : private ptr_vector_base
{
public:
    void push_back(T* p) { push_back_fwd(p); }
    void print() const { ptr_vector_base::print(); }
};
// -----------------------
// source (ptr_vector.cpp)
// #include "ptr_vector.hpp"
#include <iostream>
#include <vector>
struct ptr_vector_base::impl
{
    std::vector<void*> vp;
    void push_back(void* p)
    {
        vp.push_back(p);
    }
    void print() const
    {
        for (void const * const p: vp) std::cout << p << '\n';
    }
};
void ptr_vector_base::push_back_fwd(void* p) { pImpl->push_back(p); }
ptr_vector_base::ptr_vector_base() : pImpl{std::make_unique<impl>()} {}
ptr_vector_base::~ptr_vector_base() {}
void ptr_vector_base::print() const { pImpl->print(); }
// ---------------
// user (main.cpp)
// #include "ptr_vector.hpp"
int main()
{
    int x{}, y{}, z{};
    ptr_vector<int> v;
    v.push_back(&x);
    v.push_back(&y);
    v.push_back(&z);
    v.print();
}

Sortie possible :

0x7ffd6200a42c
0x7ffd6200a430
0x7ffd6200a434

Surcharge d'exécution

  • Surcharge d'accès : Dans pImpl, chaque appel à une fonction membre privée passe par une indirection via un pointeur. Chaque accès à un membre public effectué par un membre privé passe par une autre indirection via un pointeur. Les deux indirections traversent les limites des unités de traduction et ne peuvent donc être optimisées que par l'optimisation à l'édition des liens. Notez que la fabrique orientée objet nécessite une indirection entre les unités de traduction pour accéder aux données publiques et aux détails d'implémentation, et offre encore moins d'opportunités d'optimisation à l'édition des liens en raison de la dispatch virtuelle.
  • Surcharge d'espace : pImpl ajoute un pointeur au composant public et, si un membre privé nécessite l'accès à un membre public, un autre pointeur est soit ajouté au composant d'implémentation soit passé comme paramètre pour chaque appel au membre privé qui en a besoin. Si des allocateurs personnalisés avec état sont pris en charge, l'instance de l'allocateur doit également être stockée.
  • Surcharge de gestion de durée de vie : pImpl (ainsi que la fabrique orientée objet) place l'objet d'implémentation sur le tas, ce qui impose une surcharge d'exécution significative lors de la construction et de la destruction. Cela peut être partiellement compensé par des allocateurs personnalisés, puisque la taille d'allocation pour pImpl (mais pas pour la fabrique orientée objet) est connue au moment de la compilation.

D'autre part, les classes pImpl sont compatibles avec la sémantique de déplacement ; refactoriser une grande classe en tant que pImpl déplaçable peut améliorer les performances des algorithmes qui manipulent des conteneurs contenant de tels objets, bien que le pImpl déplaçable ait une source supplémentaire de surcharge à l'exécution : toute fonction membre publique autorisée sur un objet déplacé et nécessitant un accès à l'implémentation privée entraîne une vérification de pointeur nul.

Charge de maintenance

L'utilisation de pImpl nécessite une unité de traduction dédiée (une bibliothèque header-only ne peut pas utiliser pImpl), introduit une classe supplémentaire, un ensemble de fonctions de redirection et, si des allocateurs sont utilisés, expose le détail d'implémentation de l'utilisation d'allocateur dans l'interface publique.

Puisque les membres virtuels font partie de l'interface du pImpl, simuler un pImpl implique de simuler uniquement le composant d'interface. Un pImpl testable est généralement conçu pour permettre une couverture de test complète via l'interface disponible.

Implémentation

Comme l'objet de type interface contrôle la durée de vie de l'objet de type implémentation, le pointeur vers l'implémentation est généralement std::unique_ptr .

Parce que std::unique_ptr exige que le type pointé soit un type complet dans tout contexte où le suppresseur est instancié, les fonctions membres spéciales doivent être déclarées par l'utilisateur et définies hors ligne, dans le fichier d'implémentation, où la classe d'implémentation est complète.

Parce que lorsqu'une fonction membre const appelle une fonction via un pointeur de membre non-const, la surcharge non-const de la fonction d'implémentation est appelée, le pointeur doit être encapsulé dans std::experimental::propagate_const ou équivalent.

Tous les membres de données privés et toutes les fonctions membres non virtuelles privées sont placés dans la classe d'implémentation. Tous les membres publics, protégés et virtuels restent dans la classe d'interface (voir GOTW #100 pour la discussion des alternatives).

Si l'un des membres privés doit accéder à un membre public ou protégé, une référence ou un pointeur vers l'interface peut être transmis à la fonction privée en tant que paramètre. Alternativement, la référence inverse peut être maintenue comme partie de la classe d'implémentation.

Si des allocateurs non par défaut sont destinés à être pris en charge pour l'allocation de l'objet d'implémentation, l'un des modèles habituels de sensibilisation aux allocateurs peut être utilisé, y compris le paramètre de modèle d'allocateur par défaut à std::allocator et l'argument de constructeur de type std::pmr::memory_resource* .

Notes

Exemple

Démontre un pImpl avec propagation de const, avec une référence arrière passée en paramètre, sans prise en charge d'allocateur, et avec support du déplacement sans vérifications à l'exécution :

// ----------------------
// interface (widget.hpp)
#include <experimental/propagate_const>
#include <iostream>
#include <memory>
class widget
{
    class impl;
    std::experimental::propagate_const<std::unique_ptr<impl>> pImpl;
public:
    void draw() const; // API publique qui sera transmise à l'implémentation
    void draw();
    bool shown() const { return true; } // API publique que l'implémentation doit appeler
    widget(); // même le constructeur par défaut doit être défini dans le fichier d'implémentation
              // Note : appeler draw() sur un objet construit par défaut est un comportement indéfini
    explicit widget(int);
    ~widget(); // défini dans le fichier d'implémentation, où impl est un type complet
    widget(widget&&); // défini dans le fichier d'implémentation
                      // Note : appeler draw() sur un objet déplacé est un comportement indéfini
    widget(const widget&) = delete;
    widget& operator=(widget&&); // défini dans le fichier d'implémentation
    widget& operator=(const widget&) = delete;
};
// ---------------------------
// implementation (widget.cpp)
// #include "widget.hpp"
class widget::impl
{
    int n; // données privées
public:
    void draw(const widget& w) const
    {
        if (w.shown()) // cet appel à une fonction membre publique nécessite la référence arrière
            std::cout << "drawing a const widget " << n << '\n';
    }
    void draw(const widget& w)
    {
        if (w.shown())
            std::cout << "drawing a non-const widget " << n << '\n';
    }
    impl(int n) : n(n) {}
};
void widget::draw() const { pImpl->draw(*this); }
void widget::draw() { pImpl->draw(*this); }
widget::widget() = default;
widget::widget(int n) : pImpl{std::make_unique<impl>(n)} {}
widget::widget(widget&&) = default;
widget::~widget() = default;
widget& widget::operator=(widget&&) = default;
// ---------------
// user (main.cpp)
// #include "widget.hpp"
int main()
{
    widget w(7);
    const widget w2(8);
    w.draw();
    w2.draw();
}

Sortie :

drawing a non-const widget 7
drawing a const widget 8

Liens externes

1. GotW #28 : L'idiome Fast Pimpl.
2. GotW #100 : Pare-feux de compilation.
3. Le motif Pimpl - ce que vous devez savoir.