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" oder "pImpl" ist eine C++ Programmiertechnik , die Implementierungsdetails einer Klasse aus ihrer Objektrepräsentation entfernt, indem sie in eine separate Klasse ausgelagert werden, auf die über einen opaken Zeiger zugegriffen wird:

// --------------------
// Schnittstelle (widget.h)
struct widget
{
    // öffentliche Member
private:
    struct impl; // Vorwärtsdeklaration der Implementierungsklasse
    // Ein Implementierungsbeispiel: siehe unten für andere Designoptionen und Kompromisse
    std::experimental::propagate_const< // const-weiterleitender Zeiger-Wrapper
        std::unique_ptr<                // Opaque-Zeiger mit exklusiver Besitzerschaft
            impl>> pImpl;               // zur vorwärtsdeklarierten Implementierungsklasse
};
// ---------------------------
// Implementierung (widget.cpp)
struct widget::impl
{
    // Implementierungsdetails
};

Diese Technik wird verwendet, um C++-Bibliotheksschnittstellen mit stabiler ABI zu konstruieren und Kompilierzeitabhängigkeiten zu reduzieren.

Inhaltsverzeichnis

Erklärung

Da private Datenelemente einer Klasse an ihrer Objektdarstellung teilnehmen, was Größe und Layout beeinflusst, und da private Elementfunktionen einer Klasse an der Überladungsauflösung teilnehmen (die vor der Zugriffsprüfung stattfindet), erfordert jede Änderung dieser Implementierungsdetails eine Neukompilierung aller Benutzer der Klasse.

pImpl entfernt diese Kompilierungsabhängigkeit; Änderungen an der Implementierung verursachen keine Neukompilierung. Folglich, wenn eine Bibliothek pImpl in ihrer ABI verwendet, können neuere Versionen der Bibliothek die Implementierung ändern, während sie ABI-kompatibel mit älteren Versionen bleiben.

Kompromisse

Die Alternativen zum pImpl-Idiom sind

  • Inline-Implementierung: Private und öffentliche Member sind Member derselben Klasse.
  • Reine abstrakte Klasse (OOP-Factory): Benutzer erhalten einen eindeutigen Zeiger auf eine schlanke oder abstrakte Basisklasse, die Implementierungsdetails befinden sich in der abgeleiteten Klasse, die ihre virtuellen Memberfunktionen überschreibt.

Kompilations-Firewall

In einfachen Fällen entfernen sowohl pImpl als auch die Fabrikmethode die Kompilierungszeitabhängigkeit zwischen der Implementierung und den Benutzern der Klassenschnittstelle. Die Fabrikmethode erzeugt eine versteckte Abhängigkeit von der vtable, sodass das Neuanordnen, Hinzufügen oder Entfernen virtueller Memberfunktionen die ABI bricht. Der pImpl-Ansatz hat keine versteckten Abhängigkeiten, jedoch wenn die Implementierungsklasse eine Klassentemplatespezialisierung ist, geht der Vorteil der Kompilierungsfirewall verloren: Die Benutzer der Schnittstelle müssen die gesamte Templatedefinition einsehen, um die korrekte Spezialisierung zu instanziieren. Ein gängiger Designansatz in diesem Fall ist die Refaktorierung der Implementierung so, dass Parametrisierung vermieden wird, dies ist ein weiterer Anwendungsfall für die C++ Core Guidelines:

Beispielsweise verwendet die folgende Klassenvorlage den Typ T nicht in ihren privaten Mitgliedern oder im Rumpf von push_back :

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

Daher können private Member unverändert an die Implementierung übertragen werden, und push_back kann an eine Implementierung weiterleiten, die ebenfalls kein T in der Schnittstelle verwendet:

// ---------------------
// header (ptr_vector.hpp)
#include <memory>
class ptr_vector_base
{
    struct impl; // does not depend on T
    std::unique_ptr<impl> pImpl;
protected:
    void push_back_fwd(void*);
    void print() const;
    // ... see implementation section for special member functions
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();
}

Mögliche Ausgabe:

0x7ffd6200a42c
0x7ffd6200a430
0x7ffd6200a434

Laufzeit-Overhead

  • Zugriffsaufwand: Bei pImpl indirektiert jeder Aufruf einer privaten Memberfunktion über einen Zeiger. Jeder Zugriff auf ein öffentliches Member, der durch ein privates Member erfolgt, indirektiert über einen weiteren Zeiger. Beide Indirektionen überschreiten Übersetzungseinheitengrenzen und können daher nur durch Link-Time-Optimierung wegoptimiert werden. Beachten Sie, dass OO-Factory eine Indirektion über Übersetzungseinheiten hinweg erfordert, um sowohl auf öffentliche Daten als auch auf Implementierungsdetails zuzugreifen, und bietet aufgrund des virtuellen Dispatchs noch weniger Möglichkeiten für den Link-Time-Optimierer.
  • Speicheraufwand: pImpl fügt eine Zeiger zur öffentlichen Komponente hinzu und, falls ein privates Member auf ein öffentliches Member zugreifen muss, wird entweder ein weiterer Zeiger zur Implementierungskomponente hinzugefügt oder als Parameter für jeden Aufruf des privaten Members, der ihn benötigt, übergeben. Wenn zustandsbehaftete benutzerdefinierte Allokatoren unterstützt werden, muss auch die Allokatorinstanz gespeichert werden.
  • Lebensdauerverwaltungsaufwand: pImpl (sowie OO-Factory) platziert das Implementierungsobjekt auf dem Heap, was erheblichen Laufzeitaufwand bei Konstruktion und Destruktion verursacht. Dies kann teilweise durch benutzerdefinierte Allokatoren ausgeglichen werden, da die Allokationsgröße für pImpl (aber nicht für OO-Factory) zur Kompilierzeit bekannt ist.

Andererseits sind pImpl-Klassen bewegungsfreundlich; die Refaktorisierung einer großen Klasse als bewegliches pImpl kann die Leistung von Algorithmen verbessern, die Container mit solchen Objekten manipulieren, obwohl bewegliches pImpl eine zusätzliche Laufzeitüberlastung hat: Jede öffentliche Memberfunktion, die für ein verschobenes Objekt zulässig ist und Zugriff auf die private Implementierung benötigt, verursacht eine Nullzeigerprüfung.

Wartungsaufwand

Die Verwendung von pImpl erfordert eine dedizierte Übersetzungseinheit (eine Header-only-Bibliothek kann pImpl nicht verwenden), führt eine zusätzliche Klasse ein, eine Reihe von Weiterleitungsfunktionen, und, falls Allokatoren verwendet werden, offenbart die Implementierungsdetails der Allokatorverwendung in der öffentlichen Schnittstelle.

Da virtuelle Mitglieder Teil der Schnittstellenkomponente von pImpl sind, bedeutet das Mocking eines pImpl das Mocking ausschließlich der Schnittstellenkomponente. Ein testbares pImpl ist in der Regel so konzipiert, dass vollständige Testabdeckung über die verfügbare Schnittstelle ermöglicht wird.

Implementierung

Da das Objekt des Schnittstellentyps die Lebensdauer des Objekts des Implementierungstyps steuert, ist der Zeiger auf die Implementierung üblicherweise std::unique_ptr .

Da std::unique_ptr erfordert, dass der referenzierte Typ in jedem Kontext, in dem der Deleter instanziiert wird, ein vollständiger Typ ist, müssen die speziellen Elementfunktionen benutzerdefiniert deklariert und außerhalb der Klasse in der Implementierungsdatei definiert werden, wo die Implementierungsklasse vollständig ist.

Denn wenn eine const-Memberfunktion eine Funktion über einen nicht-konstanten Memberzeiger aufruft, die nicht-konstante Überladung der Implementierungsfunktion aufgerufen wird, muss der Zeiger in std::experimental::propagate_const oder Äquivalent eingewickelt werden.

Alle privaten Datenmitglieder und alle privaten nicht-virtuellen Elementfunktionen werden in der Implementierungsklasse platziert. Alle öffentlichen, geschützten und virtuellen Mitglieder bleiben in der Interface-Klasse (siehe GOTW #100 für die Diskussion der Alternativen).

Wenn eines der privaten Mitglieder auf ein öffentliches oder geschütztes Mitglied zugreifen muss, kann eine Referenz oder ein Zeiger auf die Schnittstelle als Parameter an die private Funktion übergeben werden. Alternativ kann die Rückreferenz als Teil der Implementierungsklasse beibehalten werden.

Wenn nicht-standardmäßige Allokatoren für die Allokation des Implementierungsobjekts unterstützt werden sollen, können beliebige der üblichen Allokator-Awareness-Muster verwendet werden, einschließlich eines Allokator-Template-Parameters, der standardmäßig auf std::allocator gesetzt ist, und eines Konstruktorarguments vom Typ std::pmr::memory_resource* .

Hinweise

Beispiel

Demonstriert ein pImpl mit const-Propagation, mit Back-Reference als Parameter übergeben, ohne Allocator-Awareness und Move-fähig ohne Laufzeitprüfungen:

// ----------------------
// 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; // public API that will be forwarded to the implementation
    void draw();
    bool shown() const { return true; } // public API that implementation has to call
    widget(); // even the default ctor needs to be defined in the implementation file
              // Note: calling draw() on default constructed object is UB
    explicit widget(int);
    ~widget(); // defined in the implementation file, where impl is a complete type
    widget(widget&&); // defined in the implementation file
                      // Note: calling draw() on moved-from object is UB
    widget(const widget&) = delete;
    widget& operator=(widget&&); // defined in the implementation file
    widget& operator=(const widget&) = delete;
};
// ---------------------------
// implementation (widget.cpp)
// #include "widget.hpp"
class widget::impl
{
    int n; // private data
public:
    void draw(const widget& w) const
    {
        if (w.shown()) // this call to public member function requires the back-reference 
            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();
}

Ausgabe:

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

Externe Links

1. GotW #28 : The Fast Pimpl Idiom.
2. GotW #100 : Compilation Firewalls.
3. The Pimpl Pattern - was Sie wissen sollten.