Das Pimpl-Programmiermuster - Was Sie wissen sollten

Grundlagen

Sie können auf die Pimpl-Vorlage unter anderen Namen stoßen: d-Zeiger, Compiler-Firewall oder sogar Cheshire Cat-Vorlage oder undurchsichtiger Zeiger.

In seiner Hauptform sieht die Vorlage so aus:

  • In einer Klasse verschieben wir alle privaten Member in einen neu deklarierten Typ, wie z. B. die PrivateImpl -Klasse.
  • Wir deklarieren PrivateImpl in der Header-Datei der Hauptklasse.
  • In der entsprechenden cpp-Datei deklarieren wir die Klasse PrivateImpl und definieren sie.
  • Wenn Sie jetzt die private Implementierung ändern, wird der Client-Code nicht neu kompiliert (weil sich die Schnittstelle nicht geändert hat).

Es könnte also so aussehen (roher, alter Codestil!):

Klasse.h

class MyClassImpl;
class MyClass {
    // ...
    void Foo();
private:    
    MyClassImpl* m_pImpl; // Предупреждение!!! 
                          // raw-указатель! :)
};

class.cpp

class MyClassImpl
{
public:
    void DoStuff() { /*...*/ }
};

MyClass::MyClass () 
: m_pImpl(new MyClassImpl()) 
{ }

MyClass::~MyClass () { delete m_pImpl; }

void MyClass ::DoSth() {
    m_pImpl->DoSth();
}

Eh ... hässliche rohe Zeiger!

Kurz gesagt: Wir packen alles, was privat ist, in diese vorwärts deklarierte Klasse. Wir verwenden nur ein Mitglied unserer Hauptklasse - der Compiler kann nur mit einem Zeiger ohne vollständige Typdeklaration arbeiten, da nur die Größe des Zeigers benötigt wird. Die Deklaration und Implementierung erfolgt dann in der .cpp -Datei.

Natürlich wird in modernem C++ empfohlen, unique_ptr anstelle von rohen Zeigern zu verwenden.

Die beiden offensichtlichen Nachteile dieses Ansatzes bestehen darin, dass wir Speicher zuweisen müssen, um den privaten Abschnitt zu speichern. Und auch die Hauptklasse leitet den Methodenaufruf einfach an die private Implementierung um.

Okay ... aber das ist es ... richtig? Nicht so einfach!

Der obige Code könnte funktionieren, aber wir müssen ein paar Bits hinzufügen, damit er im wirklichen Leben funktioniert.


Mehr Code

Wir müssen ein paar Fragen stellen, bevor wir den vollständigen Code schreiben können:

  • Ist Ihre Klasse kopierbar oder nur verschiebbar?
  • wie man const für Methoden in dieser privaten Implementierung erzwingt?
  • Benötigen Sie einen "Rückwärts"-Zeiger, damit die Impl-Klasse Mitglieder der Hauptklasse aufrufen/referenzieren kann?
  • Was sollte in dieser privaten Implementierung enthalten sein? alles was privat ist?

Der erste Teil, Kopieren/Verschiebbarkeit, bezieht sich auf die Tatsache, dass wir mit einem einfachen Rohzeiger ein Objekt nur oberflächlich kopieren können. Dies geschieht natürlich jedes Mal, wenn Sie einen Zeiger in Ihrer Klasse haben.

Wir müssen also unbedingt einen Kopierkonstruktor implementieren (oder ihn entfernen, wenn wir nur einen verschiebbaren Typ haben wollen).

Was ist mit dem Problem mit const ? Kannst du es im Hauptbeispiel erkennen?

Wenn Sie eine const -Methode deklarieren, können Sie die Mitglieder des Objekts nicht ändern. Mit anderen Worten, sie werden zu const . Aber das ist ein Problem für unser m_pImpl , das ein Zeiger ist. In einer const -Methode wird dieser Zeiger auch zu const, was bedeutet, dass wir ihm keinen anderen Wert zuweisen können ... aber ... wir können problemlos alle Methoden dieser privaten Basisklasse aufrufen (nicht nur Konstanten )!

Wir brauchen also einen Konvertierungs-/Wrapper-Mechanismus. Sowas in der Art:

const MyClassImpl* Pimpl() const { return m_pImpl; }
MyClassImpl* Pimpl() { return m_pImpl; }

Und jetzt sollten wir in allen unseren Hauptklassenmethoden diese Wrapper-Funktion verwenden und nicht den Zeiger selbst.

Bisher habe ich diesen "umgekehrten" Zeiger ("q-Zeiger" in der Qt-Terminologie) noch nicht erwähnt. Die Antwort bezieht sich auf den letzten Punkt – was sollten wir in eine private Implementierung einfügen – nur private Felder? Oder vielleicht sogar private Veranstaltungen?

Grundlegender Code zeigt diese praktischen Probleme nicht. Aber in einer echten Anwendung kann eine Klasse viele Methoden und Felder enthalten. Ich habe Beispiele gesehen, bei denen der gesamte private Teil (unter Verwendung von Methoden) an die Klasse pimpl übergeben wurde. Manchmal muss die Pimpl -Klasse jedoch die "echte" Methode der Hauptklasse aufrufen, also müssen wir diesen "Rückwärts"-Zeiger bereitstellen. Dies kann in der Konstruktion erfolgen, übergeben Sie einfach einen Zeiger auf this .

Verbesserte Version

Hier ist also eine verbesserte Version unseres Beispielcodes:

Klasse.h

class MyClassImpl;
class MyClass
{
public:
    explicit MyClass();
    ~MyClass(); 

    // movable:
    MyClass(MyClass && rhs) noexcept;   
    MyClass& operator=(MyClass && rhs) noexcept;

    // and copyable
    MyClass(const MyClass& rhs);
    MyClass& operator=(const MyClass& rhs);

    void DoSth();
    void DoConst() const;

private:
    const MyClassImpl* Pimpl() const { return m_pImpl.get(); }
    MyClassImpl* Pimpl() { return m_pImpl.get(); }

    std::unique_ptr<MyClassImpl> m_pImpl;
};

class.cpp

class MyClassImpl
{
public:
    ~MyClassImpl() = default;

    void DoSth() { }
    void DoConst() const { }
};

MyClass::MyClass() : m_pImpl(new MyClassImpl()) 
{

}

MyClass::~MyClass() = default;
MyClass::MyClass(MyClass &&) noexcept = default;
MyClass& MyClass::operator=(MyClass &&) noexcept = default;

MyClass::MyClass(const MyClass& rhs)
    : m_pImpl(new MyClassImpl(*rhs.m_pImpl))
{}

MyClass& MyClass::operator=(const MyClass& rhs) {
    if (this != &rhs) 
        m_pImpl.reset(new MyClassImpl(*rhs.m_pImpl));

    return *this;
}

void MyClass::DoSth()
{
    Pimpl()->DoSth();
}

void MyClass::DoConst() const
{
    Pimpl()->DoConst();
}

Jetzt etwas besser.

Der obige Code verwendet

  • unique_ptr - aber beachten Sie, dass der Destruktor für die Hauptklasse in der cpp-Datei definiert werden muss. Andernfalls beschwert sich der Compiler darüber, dass der fehlende Typ entfernt wird ...
  • Die Klasse ist beweglich und kopierbar, da vier Methoden definiert sind
  • Um bei konstanten Methoden sicher zu sein, verwenden alle Proxy-Methoden der Hauptklasse die Pimpl() -Methode, um den richtigen Zeigertyp zu erhalten.

Werfen Sie einen Blick auf diesen Block Pimp My Pimpl - Reloaded für weitere Informationen über Pimpl.

Sie können hier mit dem vollständigen Beispiel herumspielen (es gibt auch einige nette Dinge zu lernen).

cpp_pimpl2.h

#include <memory>

class MyClassImpl;
class MyClass
{
public:
    explicit MyClass();
    ~MyClass(); 

    // movable:
    MyClass(MyClass && rhs) noexcept;   
    MyClass& operator=(MyClass && rhs) noexcept;

    // and copyable:
    MyClass(const MyClass& rhs);
    MyClass& operator=(const MyClass& rhs);

    void DoSth();
    void DoConst() const;

private:
    // const access:
    const MyClassImpl* Pimpl() const { return m_pImpl.get(); }
    MyClassImpl* Pimpl() { return m_pImpl.get(); }

    std::unique_ptr<MyClassImpl> m_pImpl;
};

cpp_pimpl2.cpp

#include "cpp_pimpl2.h"
#include <iostream>

void* operator new(size_t count) {
    std::cout << "allocating " << count << " bytes\n";
    return malloc(count);
}

void operator delete(void* ptr) noexcept {
    std::cout << "global op delete called\n";
    free(ptr);
}

class MyClassImpl
{
public:
    MyClassImpl(MyClass* pBackPtr) : m_pMainClass(pBackPtr) { }
    ~MyClassImpl() = default;

    void DoSth() { std::cout << "Val (incremented): " << ++m_val << "\n";}

    // try to uncomment (or comment 'const' for the method)
    void DoConst() const { 
        std::cout << "Val: " << /*++*/m_val << "\n"; 
    } 

private:
    int m_val {0};
    MyClass* m_pMainClass {nullptr}; // back pointer
};

MyClass::MyClass() : m_pImpl(new MyClassImpl(this)) 
{
    std::cout <<  __PRETTY_FUNCTION__ << "\n";
}

MyClass::~MyClass() { std::cout <<  __PRETTY_FUNCTION__ << "\n"; }
MyClass::MyClass(MyClass &&) noexcept = default;
MyClass& MyClass::operator=(MyClass &&) noexcept = default;

MyClass::MyClass(const MyClass& rhs)
    : m_pImpl(new MyClassImpl(*rhs.m_pImpl))
{}

MyClass& MyClass::operator=(const MyClass& rhs) {
    if (this != &rhs) 
        m_pImpl.reset(new MyClassImpl(*rhs.m_pImpl));

    return *this;
}

void MyClass::DoSth()
{
    Pimpl()->DoSth();
}

void MyClass::DoConst() const
{
    Pimpl()->DoConst();
}

cpp_pimpl2_client.cpp

#include "cpp_pimpl2.h"
#include <iostream>
#include <vector>

int main()
{
    MyClass myObject;
    myObject.DoSth();

    const MyClass secondObject;
    secondObject.DoConst();
}

Wie Sie sehen können, gibt es hier einen Code, der eine Vorlage ist. Aus diesem Grund gibt es mehrere Ansätze, um dieses Idiom in eine separate Utility-Klasse zu packen. Mal sehen unten.

Als separate Klasse

Für das Herb-Sutter-Beispiel schlägt GotW #101: Compilation Firewalls, Part 2 den folgenden Wrapper vor.

genommenFromHerbSutter.h

template<typename T>
class pimpl {
private:
    std::unique_ptr<T> m;
public:
    pimpl();
    template<typename ...Args> pimpl( Args&& ... );
    ~pimpl();
    T* operator->();
    T& operator*();
};

Ihnen bleibt jedoch bei Bedarf die Implementierung des Kopierkonstruktors.

Wenn Sie einen vollständigen Wrapper wünschen, werfen Sie einen Blick auf diesen Beitrag PIMPL, Rule of Zero und Scott Meyers von Andrey Upadyshev.

In diesem Artikel sehen Sie eine sehr fortgeschrittene Implementierung eines solchen Hilfstyps:

SPIMPL (Smart Pointer to IMPLementation) ist eine kleine Header-only-C++11-Bibliothek zur Vereinfachung der Implementierung des PIMPL-Idioms

Innerhalb der Bibliothek finden Sie zwei Typen: 1) spimpl::unique_impl_ptr - nur für pimpl verschiebbare Wrapper und 2) spimpl::impl_ptr für pimpl verschiebbare und kopierbare Wrapper.

Schnelle Pickel

Ein offensichtlicher Punkt bei impl ist, dass Speicherzuordnung benötigt wird, um die privaten Teile der Klasse zu speichern. Wenn Sie es vermeiden möchten ... und Ihnen diese Speicherzuweisung wirklich wichtig ist ... können Sie Folgendes versuchen:

  • Stellen Sie einen benutzerdefinierten Zuordner bereit und verwenden Sie einen Teil des festen Speichers für eine private Implementierung
  • oder reservieren Sie einen großen Speicherblock in der Hauptklasse und verwenden Sie die neue Zuordnung, um Platz für pimpl zuzuweisen.
  • Beachten Sie, dass das Reservieren von Platz im Voraus unzuverlässig ist - was ist, wenn sich die Größe ändert? Und noch wichtiger - haben Sie die richtige Ausrichtung für den Typ?

Herb Sutter schrieb über diese Idee hier GotW #28: The Fast Pimpl Idiom .

Die moderne Version mit dem C++11-Feature aligned_storage wird hier beschrieben:
My Favourite C++ Idiom: Static PIMPL / Fast PIMPL von Kai Dietrich oder Typsichere Pimpl-Implementierung ohne Overhead | Wahrscheinlich Tanz-Blog .

Aber denken Sie daran, dass dies nur ein Trick ist, es könnte nicht funktionieren. Oder es funktioniert möglicherweise auf einer Plattform/einem Compiler, aber nicht auf einer anderen Konfiguration.

Diese Vorgehensweise finde ich persönlich nicht gut. Pimp wird normalerweise für größere Klassen verwendet (vielleicht Manager, Typen in Modulschnittstellen), sodass die zusätzlichen Kosten nicht viel ausmachen.

Wir haben mehrere Hauptteile des Noppenmusters gesehen, also können wir jetzt seine Stärken und Schwächen diskutieren.

Vor-und Nachteile

Zum

  • Bietet eine Kompilierungs-Firewall : Wenn eine private Implementierung den Client-Code ändert, ist keine Neukompilierung erforderlich.
  • Header können kleiner werden, da Typen, die nur in der Klassenimplementierung erwähnt werden, nicht mehr für Client-Code definiert werden müssen.
  • Im Allgemeinen kann es also zu besseren Kompilierzeiten führen.
  • Bietet Binärkompatibilität : sehr wichtig für Bibliotheksentwickler. Solange die binäre Schnittstelle gleich bleibt, können Sie Ihre Anwendung mit einer anderen Version der Bibliothek verknüpfen.
  • Zur Vereinfachung ändert sich die ABI, wenn Sie eine neue virtuelle Methode hinzufügen, aber das Hinzufügen nicht virtueller Methoden (ohne natürlich vorhandene zu entfernen) ändert die ABI nicht.
  • Смотрите Fragile Binary Interface Problem .
  • Möglicher Vorteil: keine V-Tabelle (wenn die Hauptklasse nur nicht-virtuelle Methoden enthält).
  • Kleiner Punkt: Kann als Objekt auf dem Stapel verwendet werden

Gegen

  • Leistung - eine Umleitungsebene hinzugefügt.
  • Für eine private Implementierung muss ein Speicherblock zugewiesen (oder vorab zugewiesen) werden.
  • Mögliche Speicherfragmentierung
  • Komplexer Code und erfordert etwas Disziplin, um solche Klassen zu pflegen.
  • Debugging - Sie sehen die Details nicht sofort, die Klasse ist geteilt

Andere Fragen

  • Testbarkeit - Es besteht die Auffassung, dass es Probleme geben kann, wenn Sie versuchen, eine solche Pimpl-Klasse zu testen. Aber im Allgemeinen überprüfen Sie nur die öffentliche Schnittstelle, es spielt keine Rolle.
  • Nicht für jede Klasse. Dieses Muster eignet sich oft besser für große Klassen auf der "Schnittstellenebene". Ich glaube nicht, dass vector3d mit dieser Vorlage eine gute Idee wäre ...

Alternativen

  • Überarbeiten Sie den Code
    *Um die Bauzeit zu verbessern:
  • Verwenden Sie vorkompilierte Header-Dateien
  • Verwenden Sie Build-Caches
  • Verwenden Sie den inkrementellen Build-Modus
  • Abstrakte Schnittstellen
  • Bietet keine ABI-Kompatibilität, ist aber eine großartige Alternative als Technologie zum Aufbrechen von Abhängigkeiten
  • Gamasutra - Ausführlich: PIMPL vs. rein virtuelle Schnittstellen
  • MIT
  • basiert ebenfalls auf abstrakten Schnittstellen, aber mit einigen grundlegenderen Mechanismen.

Wie wäre es mit modernem C++

Ab C++17 haben wir keine neuen Funktionen speziell für Pimpl. Mit C++11 haben wir intelligente Zeiger, also versuchen Sie, Pimpl mit intelligenten Zeigern anstelle von rohen Zeigern zu implementieren. Außerdem bekommen wir natürlich jede Menge Metaprogrammierungs-Zeug, um beim Deklarieren von Wrapper-Typen für das Pimpl-Template zu helfen.

Aber in Zukunft sollten wir zwei Optionen in Betracht ziehen: Module und den Punktoperator.

Module werden eine wichtige Rolle bei der Reduzierung der Kompilierzeit spielen. Ich habe nicht viel mit Modulen gespielt, aber meiner Meinung nach wird die Verwendung von Pimpl nur für die Kompilierungsgeschwindigkeit weniger kritisch. Natürlich ist es immer wichtig, die Abhängigkeiten gering zu halten.

Eine weitere Funktion, die sich als nützlich erweisen könnte, ist der Punktoperator, der von Bjarne Stroustup und Gabriel Dos Reis entwickelt wurde. PDF - N4477 - nicht in C++17 erstellt, aber vielleicht sehen wir es in C++20?

Grundsätzlich können Sie damit den Punktoperator überschreiben und viel bequemeren Code für alle Proxy-Typen bereitstellen.

Wer benutzt

Folgende Beispiele habe ich gesammelt:

  • QT:
  • Dies sind wahrscheinlich die auffälligsten Beispiele (die Sie in der Public Domain finden können), in denen proprietäre Implementierungen weit verbreitet waren.
  • Es gibt sogar einen guten Einführungsartikel über d-Pointer (wie sie Pimpl nennen): Pointer – Qt Wiki
  • QT zeigt auch, wie man Pimpl mit Vererbung verwendet. Theoretisch benötigen Sie für jede abgeleitete Klasse einen separaten Pimpl, aber QT verwendet nur einen Zeiger.
  • OpenSceneGraph
  • Framebuffer-Objekt
  • Assimp-Bibliothek
  • Exporter
  • Schau dir diesen Kommentar in assimp.hpp an :)
// Священный материал, только для членов высшего совета джедаев.
class ImporterPimpl;

// ...

// Просто потому, что мы не хотим, чтобы вы знали, как мы взламываем.
ImporterPimpl* pimpl;

Sieht so aus, als ob das Muster überall verwendet wird :)

Lassen Sie mich wissen, wenn Sie andere Beispiele haben.

Wenn Sie weitere Beispiele benötigen, folgen Sie diesen beiden Fragen zum Stapelüberlauf:

Fazit

Pimpl sieht einfach aus... aber wie üblich in C++ sind die Dinge in der Praxis nicht so einfach :)

Grundmomente:

  • Pimpl bietet ABI-Kompatibilität und reduziert Kompilierungsabhängigkeiten.
  • Seit C++11 müssen Sie unique_ptr (oder sogar shared_ptr) verwenden, um ein Template zu implementieren.
  • Damit das Muster funktioniert, entscheiden Sie, ob Ihre Hauptklasse kopierbar oder nur verschiebbar sein soll.
  • Achten Sie auf const-Methoden, damit die private Implementierung sie berücksichtigt.
  • Wenn die private Implementierung Zugriff auf die primären Mitglieder der Klasse erfordert, wird ein "Rückwärtszeiger" benötigt.
  • Einige Optimierungen sind möglich (um eine geteilte Speicherzuweisung zu vermeiden), aber das kann schwierig sein.
  • Es gibt viele Verwendungen des Musters in Open-Source-Projekten, QT verwendet es stark (mit Vererbung und Rückwärtszeiger).

Artikel verfasst von: Bartek | Montag, 8. Januar 2018

Рекомендуємо хостинг TIMEWEB
Рекомендуємо хостинг TIMEWEB
Stabiles Hosting des sozialen Netzwerks EVILEG. Wir empfehlen VDS-Hosting für Django-Projekte.

Magst du es? In sozialen Netzwerken teilen!

Kommentare

Nur autorisierte Benutzer können Kommentare posten.
Bitte Anmelden oder Registrieren
Letzte Kommentare
ИМ
Игорь Максимов5. Oktober 2024 14:51
Django – Lektion 064. So schreiben Sie eine Python-Markdown-Erweiterung Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55. Juli 2024 18:02
QML - Lektion 016. SQLite-Datenbank und das Arbeiten damit in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
k
kmssr9. Februar 2024 02:43
Qt Linux - Lektion 001. Autorun Qt-Anwendung unter Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
Qt WinAPI - Lektion 007. Arbeiten mit ICMP-Ping in Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25. Dezember 2023 18:30
Boost - statisches Verknüpfen im CMake-Projekt unter Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
Jetzt im Forum diskutieren
J
JacobFib17. Oktober 2024 10:27
добавить qlineseries в функции Пользователь может получить любые разъяснения по интересующим вопросам, касающимся обработки его персональных данных, обратившись к Оператору с помощью электронной почты https://topdecorpro.ru…
JW
Jhon Wick1. Oktober 2024 22:52
Indian Food Restaurant In Columbus OH| Layla’s Kitchen Indian Restaurant If you're looking for a truly authentic https://www.laylaskitchenrestaurantohio.com/ , Layla’s Kitchen Indian Restaurant is your go-to destination. Located at 6152 Cleveland Ave, Colu…
КГ
Кирилл Гусарев27. September 2024 16:09
Не запускается программа на Qt: точка входа в процедуру не найдена в библиотеке DLL Написал программу на C++ Qt в Qt Creator, сбилдил Release с помощью MinGW 64-bit, бинарнику напихал dll-ки с помощью windeployqt.exe. При попытке запуска моей сбилженной программы выдаёт три оши…
F
Fynjy22. Juli 2024 11:15
при создании qml проекта Kits есть но недоступны для выбора Поставил Qt Creator 11.0.2. Qt 6.4.3 При создании проекта Qml не могу выбрать Kits, они все недоступны, хотя настроены и при создании обычного Qt Widget приложения их можно выбрать. В чем может …

Folgen Sie uns in sozialen Netzwerken