Namensräume
Varianten
Aktionen

PImpl

Von cppreference.com
< cpp‎ | Sprache
 
 
C++ Sprache
Allgemeine Themen
Kontrollfluss
Bedingte Ausführungsaussagen
if
Iterationsanweisungen (Schleifen)
for
Bereichs-for (C++11)
Sprunganweisungen
Funktionen
Funktionsdeklaration
Lambda-Funktionsausdruck
inline-Spezifizierer
Dynamische Ausnahmespezifikationen (bis C++17*)
noexcept-Spezifizierer (C++11)
Ausnahmen
Namensräume
Typen
Spezifizierer
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Speicherdauer-Spezifizierer
Initialisierung
Ausdrücke
Alternative Darstellungen
Literale
Boolesch - Ganzzahl - Gleitkommazahl
Zeichen - String - nullptr (C++11)
Benutzerdefinierte (C++11)
Dienstprogramme
Attribute (C++11)
Typen
typedef-Deklaration
Typalias-Deklaration (C++11)
Umwandlungen
Speicherzuweisung
Klassen
Klassenspezifische Funktionseigenschaften
explicit (C++11)
static

Spezielle Member-Funktionen
Templates
Sonstiges
 

"Pointer to implementation" oder "pImpl" ist eine C++ Programmiertechnik, die Implementierungsdetails einer Klasse aus ihrer Objektrepräsentation entfernt, indem sie in einer separaten Klasse platziert werden, auf die über einen undurchsichtigen Zeiger zugegriffen wird.

// --------------------
// interface (widget.h)
struct widget
{
    // public members
private:
    struct impl; // forward declaration of the implementation class
    // One implementation example: see below for other design options and trade-offs
    std::experimental::propagate_const< // const-forwarding pointer wrapper
        std::unique_ptr<                // unique-ownership opaque pointer
            impl>> pImpl;               // to the forward-declared implementation class
};
 
// ---------------------------
// implementation (widget.cpp)
struct widget::impl
{
    // implementation details
};

Diese Technik wird verwendet, um C++-Bibliothekschnittstellen mit stabiler ABI (Application Binary Interface) zu erstellen und Kompilierungsabhängigkeiten zu reduzieren.

Inhalt

[bearbeiten] Erklärung

Da private Datenmember einer Klasse an ihrer Objektrepräsentation beteiligt sind, was Größe und Layout beeinflusst, und da private Memberfunktionen einer Klasse an der Überladungsauflösung beteiligt sind (die vor der Überprüfung des Memberzugriffs stattfindet), erfordert jede Änderung dieser Implementierungsdetails eine Neukompilierung aller Benutzer der Klasse.

pImpl entfernt diese Kompilierungsabhängigkeit; Änderungen an der Implementierung führen nicht zu einer Neukompilierung. Folglich können neuere Versionen einer Bibliothek, die pImpl in ihrer ABI verwendet, die Implementierung ändern, während sie ABI-kompatibel mit älteren Versionen bleiben.

[bearbeiten] Kompromisse

Die Alternativen zum pImpl-Idiom sind:

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

[bearbeiten] Kompilierungs-Firewall

In einfachen Fällen entfernen sowohl pImpl als auch Factory Method Kompilierungsabhängigkeiten zwischen der Implementierung und den Benutzern der Klassenschnittstelle. Die Factory-Methode erstellt eine versteckte Abhängigkeit von der vtable, und so brechen das Neuordnen, Hinzufügen oder Entfernen virtueller Memberfunktionen die ABI. Der pImpl-Ansatz hat keine versteckten Abhängigkeiten. Wenn die Implementierungsklasse jedoch eine Spezialisierung einer Klassenvorlage ist, geht der Vorteil der Kompilierungs-Firewall verloren: Die Benutzer der Schnittstelle müssen die gesamte Vorlagendefinition beobachten, um die korrekte Spezialisierung zu instanziieren. Ein gängiger Designansatz in diesem Fall ist die Refaktorierung der Implementierung auf eine Weise, die eine Parametrisierung vermeidet. Dies ist ein weiterer Anwendungsfall für die C++ Core Guidelines.

Zum Beispiel verwendet die folgende Klassenvorlage den Typ `T` nicht in ihrem privaten Member oder im Körper 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 in die Implementierung übertragen werden, und `push_back` kann an eine Implementierung weitergeleitet werden, die `T` auch in der Schnittstelle nicht 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

[bearbeiten] Laufzeit-Overhead

  • Zugriffsaufwand: Bei pImpl indirekt jeder Aufruf einer privaten Memberfunktion über einen Zeiger. Jeder Zugriff auf ein öffentliches Mitglied, das von einem privaten Mitglied gemacht wird, indirekt über einen weiteren Zeiger. Beide Indirektionen überschreiten Übersetzungsbereichsgrenzen und können daher nur durch Link-Time-Optimierung herausoptimiert werden. Beachten Sie, dass die OO-Factory eine Indirektion über Übersetzungsbereichsgrenzen erfordert, um sowohl auf öffentliche Daten als auch auf Implementierungsdetails zuzugreifen, und aufgrund der virtuellen Weiterleitung sogar weniger Möglichkeiten für den Link-Time-Optimizer bietet.
  • Speicheraufwand: pImpl fügt dem öffentlichen Teil einen Zeiger hinzu, und wenn ein privates Mitglied auf ein öffentliches Mitglied zugreifen muss, wird entweder ein weiterer Zeiger zum Implementierungsteil hinzugefügt oder als Parameter für jeden Aufruf des privaten Mitglieds übergeben, der ihn benötigt. Wenn zustandsbehaftete benutzerdefinierte Allokatoren unterstützt werden, muss auch die Allokatorinstanz gespeichert werden.
  • Overhead für die Lebensdauerverwaltung: pImpl (sowie OO Factory) platziert das Implementierungsobjekt auf dem Heap, was einen erheblichen Laufzeit-Overhead bei Konstruktion und Zerstörung mit sich bringt. Dies kann teilweise durch benutzerdefinierte Allokatoren ausgeglichen werden, da die Allokationsgröße für pImpl (nicht aber für OO Factory) zur Kompilierzeit bekannt ist.

Andererseits sind pImpl-Klassen move-freundlich; die Refaktorierung einer großen Klasse als verschiebbare pImpl kann die Leistung von Algorithmen, die Container mit solchen Objekten manipulieren, verbessern, obwohl verschiebbare pImpl eine zusätzliche Quelle für Laufzeit-Overhead aufweisen: Jede öffentliche Memberfunktion, die auf einem verschobenen Objekt zulässig ist und auf eine private Implementierung zugreifen muss, löst eine Nullzeigerprüfung aus.

[bearbeiten] Wartungsaufwand

Die Verwendung von pImpl erfordert eine dedizierte Übersetzungseinheit (eine nur-Header-Bibliothek kann pImpl nicht verwenden), führt eine zusätzliche Klasse, einen Satz von Weiterleitungsfunktionen ein und, wenn Allokatoren verwendet werden, legt sie das Implementierungsdetail der Allokatorverwendung in der öffentlichen Schnittstelle offen.

Da virtuelle Member Teil der Schnittstellenkomponente von pImpl sind, impliziert das Mocking einer pImpl das alleinige Mocking der Schnittstellenkomponente. Eine testbare pImpl ist typischerweise so konzipiert, dass sie eine vollständige Testabdeckung über die verfügbare Schnittstelle ermöglicht.

[bearbeiten] Implementierung

Da das Objekt des Schnittstellentyps die Lebensdauer des Objekts des Implementierungstyps kontrolliert, ist der Zeiger auf die Implementierung normalerweise std::unique_ptr.

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

Da ein const-Memberfunktionsaufruf eine Funktion über einen nicht-const-Memberzeiger aufruft, wird die nicht-const-Überladung der Implementierungsfunktion aufgerufen, der Zeiger muss in std::experimental::propagate_const oder einem Äquivalent gekapselt werden.

Alle privaten Datenmember und alle privaten, nicht-virtuellen Memberfunktionen werden in der Implementierungsklasse platziert. Alle öffentlichen, geschützten und virtuellen Member bleiben in der Schnittstellenklasse (siehe GOTW #100 für die Diskussion der Alternativen).

Wenn einer der privaten Member 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-Standard-Allokatoren für die Allokation des Implementierungsobjekts unterstützt werden sollen, können alle üblichen Allokator-Bewusstseins-Muster verwendet werden, einschließlich der Standardvorlage für den Allokatorparameter auf std::allocator und eines Konstruktorarguments vom Typ std::pmr::memory_resource*.

[bearbeiten] Anmerkungen

[bearbeiten] Beispiel

Demonstriert eine pImpl mit Const-Propagation, mit Rückreferenz als Parameter übergeben, ohne Allokator-Bewusstsein und verschiebbar 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

[bearbeiten] Externe Links

1.  GotW #28: Das Fast Pimpl Idiom.
2.  GotW #100: Compilation Firewalls.
3.  Das Pimpl-Muster – Was Sie wissen sollten.