Wird das PIMPL-Idiom wirklich in der Praxis verwendet?

Ich lese das Buch “Exceptional C ++” von Herb Sutter, und in diesem Buch habe ich etwas über das Pimml-Idiom gelernt. Grundsätzlich besteht die Idee darin, eine Struktur für die private Objekte einer class zu erstellen und sie dynamisch zuzuweisen, um die Kompilierungszeit zu verkürzen (und auch die privaten Implementierungen besser zu verbergen).

Beispielsweise:

 class X { private: C c; D d; } ; 

könnte geändert werden zu:

 class X { private: struct XImpl; XImpl* pImpl; }; 

und in der CPP die Definition:

 struct X::XImpl { C c; D d; }; 

Das scheint ziemlich interessant zu sein, aber ich habe noch nie zuvor einen solchen Ansatz gesehen, weder in den Unternehmen, an denen ich gearbeitet habe, noch in Open-Source-Projekten, in denen ich den Quellcode gesehen habe. Also, ich frage mich, ob diese Technik wirklich in der Praxis verwendet wird?

Sollte ich es überall oder mit Vorsicht benutzen? Und wird diese Technik in eingebetteten Systemen (wo die performance sehr wichtig ist) empfohlen?

    Also, ich frage mich, ob diese Technik wirklich in der Praxis verwendet wird? Sollte ich es überall oder mit Vorsicht benutzen?

    Natürlich wird es verwendet, und in meinem Projekt, in fast jeder class, haben Sie aus verschiedenen Gründen erwähnt:

    • Daten verstecken
    • Die Zeit für die Neukompilierung ist wirklich verringert, da nur die Quelldatei neu erstellt werden muss, aber nicht die Kopfzeile und jede Datei, die sie enthält
    • Binärkompatibilität. Da sich die classndeklaration nicht ändert, kann die Bibliothek einfach aktualisiert werden (vorausgesetzt, Sie erstellen eine Bibliothek).

    Wird diese Technik in eingebetteten Systemen empfohlen (wo die performance sehr wichtig ist)?

    Das hängt davon ab, wie stark dein Ziel ist. Die einzige Antwort auf diese Frage ist jedoch: Messen und bewerten Sie, was Sie gewinnen und verlieren.

    Es scheint, dass viele Bibliotheken es verwenden, um in ihrer API stabil zu bleiben, zumindest für einige Versionen.

    Aber was alles angeht, solltest du niemals irgendetwas überall ohne Vorbehalte benutzen. Denken Sie immer darüber nach, bevor Sie es verwenden. Bewerten Sie, welche Vorteile es Ihnen bietet, und ob sie den Preis wert sind, den Sie bezahlen.

    Die Vorteile, die es Ihnen geben kann, sind:

    • hilft bei der Aufrechterhaltung der Binärkompatibilität von gemeinsam genutzten Bibliotheken
    • versteckte interne Details
    • abnehmende Rekompilationszyklen

    Diese können oder können nicht wirklich Vorteile für Sie sein. Wie für mich ist es mir egal, ein paar Minuten Rekompilierungszeit. Endnutzer tun dies normalerweise auch nicht, da sie es immer nur von Anfang an kompilieren.

    Mögliche Nachteile sind (auch hier, abhängig von der Implementierung und ob sie echte Nachteile für Sie sind):

    • Erhöhung der Speichernutzung aufgrund mehr Zuweisungen als bei der naiven Variante
    • erhöhter Wartungsaufwand (Sie müssen mindestens die Weiterleitungsfunktionen schreiben)
    • performancesverlust (der Compiler ist möglicherweise nicht in der Lage, Dinge zu inline zu schreiben, wie es bei einer naiven Implementierung Ihrer class der Fall ist)

    Geben Sie also sorgfältig alles einen Wert und bewerten Sie es selbst. Für mich stellt sich fast immer heraus, dass die Verwendung der Pimplidiom die Mühe nicht wert ist. Es gibt nur einen Fall, in dem ich es persönlich benutze (oder zumindest etwas ähnliches):

    Mein C ++ – Wrapper für den Linux- stat Aufruf. Hier kann die Struktur aus dem C-Header unterschiedlich sein, abhängig davon, welche #defines sind. Und da meine Wrapper-Kopfzeile nicht alle kontrollieren kann, schließe ich #include in meine .cxx Datei ein und vermeide diese Probleme.

    Stimme mit allen anderen über die Waren überein, aber lassen Sie mich ein Limit angeben: funktioniert nicht gut mit Vorlagen .

    Der Grund dafür ist, dass für die Template-Instanziierung die vollständige Deklaration erforderlich ist, in der die Instanziierung stattgefunden hat. (Und das ist der Hauptgrund, warum Sie keine Template-Methoden in CPP-Dateien definiert sehen)

    Sie können immer noch auf templatetisierte Subklassen verweisen, aber da Sie sie alle einbeziehen müssen, ist jeder Vorteil der “Implementation Decoupling” beim Kompilieren verloren (es wird vermieden, dass alle platformspezifischen Codes überall enthalten sind und die Kompilierung verkürzt wird).

    Ist ein gutes Paradigma für klassische OOP (inheritance basiert), aber nicht für generische Programmierung (Spezialisierung basiert).

    Andere Leute haben bereits die technischen Up / Downside bereitgestellt, aber ich denke, das Folgende ist erwähnenswert:

    Seien Sie in erster Linie nicht dogmatisch. Wenn pImpl für Ihre Situation funktioniert, verwenden Sie es – verwenden Sie es nicht nur, weil “es ist besser, OO, da es wirklich die Implementierung versteckt” etc. Die C ++ FAQ zitieren:

    Kapselung ist für Code, nicht für Personen ( Quelle )

    Nur um Ihnen ein Beispiel für Open-Source-Software zu geben, wo es verwendet wird und warum: OpenThreads, die Threading-Bibliothek, die von OpenSceneGraph verwendet wird . Der Grundgedanke ist, den plattformspezifischen Code aus dem Header (zB ) zu entfernen, da sich interne Statusvariablen (zB Thread-Handles) von Plattform zu Plattform unterscheiden. Auf diese Weise kann man Code gegen Ihre Bibliothek kompilieren, ohne die Idiosynkrasien der anderen Plattformen zu kennen, weil alles versteckt ist.

    Ich würde PIMPL hauptsächlich für classn in Betracht ziehen, die exponiert werden, um von anderen Modulen als API verwendet zu werden. Dies hat viele Vorteile, da eine Neukompilierung der Änderungen in der PIMPL-Implementierung keinen Einfluss auf den Rest des Projekts hat. Auch für API-classn fördern sie eine Binärkompatibilität (Änderungen in einer Modulimplementierung wirken sich nicht auf Clients dieser Module aus, sie müssen nicht neu kompiliert werden, da die neue Implementierung die gleiche binäre Schnittstelle aufweist – die von der PIMPL bereitgestellte Schnittstelle).

    Was die Verwendung von PIMPL für jede class betrifft, würde ich Vorsicht walten lassen, da all diese Vorteile mit Kosten verbunden sind: Für den Zugriff auf die Implementierungsmethoden ist ein zusätzliches Maß an Indirektion erforderlich.

    Ich denke, das ist eines der grundlegendsten Werkzeuge zur Entkopplung.

    Ich habe pimpl (und viele andere Idiome aus Exceptional C ++) für ein eingebettetes Projekt (SetTopBox) verwendet.

    Der besondere Zweck dieses Idoim in unserem Projekt war es, die Verwendung der Typen der XImpl-class zu verbergen. Insbesondere haben wir es verwendet, um Details von Implementierungen für verschiedene Hardware zu verbergen, wobei verschiedene Header hineingezogen wurden. Wir hatten verschiedene Implementierungen von XImpl-classn für eine Plattform und unterschiedliche für die andere. Das Layout der class X blieb unabhängig von der Plattform gleich.

    Ich habe diese Technik in der Vergangenheit oft benutzt, aber dann habe ich mich davon entfernt.

    Natürlich ist es eine gute Idee, das Implementierungsdetail von den Benutzern Ihrer class zu verbergen. Sie können dies jedoch auch tun, indem Sie Benutzer der class dazu veranlassen, eine abstrakte Schnittstelle zu verwenden, und das Implementierungsdetail die konkrete class darstellt.

    Die Vorteile von pImpl sind:

    1. Unter der Annahme, dass es nur eine Implementierung dieser Schnittstelle gibt, ist es klarer, wenn keine abstrakte class / konkrete Implementierung verwendet wird

    2. Wenn Sie eine class von classn (ein Modul) haben, so dass mehrere classn auf dasselbe “impl” zugreifen, aber die Benutzer des Moduls nur die “exposed” classn verwenden.

    3. Keine v-Tabelle, wenn dies als eine schlechte Sache angenommen wird.

    Die Nachteile von pImpl (wo die abstrakte Schnittstelle besser funktioniert)

    1. Während Sie möglicherweise nur eine “Produktions” -Implementierung haben, können Sie mithilfe einer abstrakten Schnittstelle auch eine “Schein” -Inplementierung erstellen, die im Komponententest funktioniert.

    2. (Das größte Problem). Vor den Tagen von unique_ptr und moving hatten Sie nur eingeschränkte Möglichkeiten, pImpl zu speichern. Ein roher pointers und Probleme mit der class, die nicht kopierbar ist. Ein altes auto_ptr würde nicht mit der nach vorne deklarierten class arbeiten (nicht bei allen Compilern). Also begannen die Leute shared_ptr zu verwenden, was sehr praktisch war, um Ihre class kopierbar zu machen, aber natürlich hatten beide Kopien die gleiche zugrundeliegende shared_ptr, die Sie vielleicht nicht erwarten (modifizieren Sie eine und beide werden modifiziert). Daher bestand die Lösung darin, den rohen pointers für den inneren zu verwenden und die class nicht kopierbar zu machen und stattdessen einen shared_ptr an diesen zurückzugeben. Also zwei Anrufe zu neuen. (Tatsächlich gab 3 gegebenes altes shared_ptr Ihnen ein zweites).

    3. Technisch nicht wirklich const-correct, da die constness nicht durch einen Memberpointer weitergegeben wird.

    Im Allgemeinen bin ich daher in den Jahren von pImpl weggezogen und stattdessen in abstrakte Interface-Nutzung (und Factory-Methoden zum Erstellen von Instanzen) gegangen.

    Wie viele andere gesagt, ermöglicht das Pimpl-Idiom, vollständige Informationsverbergung und Kompilierungsunabhängigkeit zu erreichen, leider mit den Kosten des performancesverlustes (zusätzliche pointersindirektion) und zusätzlichen Speicherbedarfs (der Mitgliedszeiger selbst). Die zusätzlichen Kosten können bei der Entwicklung eingebetteter Software kritisch sein, insbesondere in den Szenarien, in denen der Speicher so weit wie möglich eingespart werden muss. Die Verwendung von abstrakten C ++ – classn als Schnittstellen würde bei gleichen Kosten zu denselben Vorteilen führen. Dies zeigt eigentlich einen großen Mangel an C ++, wo es ohne wiederkehrende C-ähnliche Schnittstellen (globale Methoden mit einem opaken pointers als Parameter) nicht möglich ist, eine wahre Informationsverbergung und Kompilierungsunabhängigkeit ohne zusätzliche Ressourcennachteile zu haben: Dies liegt hauptsächlich daran, dass Die Deklaration einer class, die von den Benutzern enthalten sein muss, exportiert nicht nur die Schnittstelle der class (öffentliche Methoden), die von den Benutzern benötigt wird, sondern auch ihre Interna (private Mitglieder), die von den Benutzern nicht benötigt werden.

    Es wird in vielen Projekten in der Praxis eingesetzt. Die Nützlichkeit hängt stark von der Art des Projekts ab. Eines der prominentesten Projekte, die dies verwenden, ist Qt , wo die Grundidee darin besteht, Implementierungs- oder plattformspezifischen Code vor dem Benutzer zu verbergen (andere Entwickler, die Qt verwenden).

    Dies ist eine noble Idee, aber es gibt einen echten Nachteil: Debugging Solange der in privaten Implementierungen verborgene Code von höchster Qualität ist, ist das alles gut, aber wenn es Bugs gibt, dann hat der Benutzer / Entwickler ein Problem, weil es nur ein blöder pointers auf eine versteckte Implementierung ist, selbst wenn er den Implementierungsquellcode hat.

    Wie bei fast allen Design-Entscheidungen gibt es ein Für und Wider.

    Ein Vorteil, den ich sehen kann, ist, dass der Programmierer bestimmte Operationen ziemlich schnell implementieren kann:

     X( X && move_semantics_are_cool ) : pImpl(NULL) { this->swap(move_semantics_are_cool); } X& swap( X& rhs ) { std::swap( pImpl, rhs.pImpl ); return *this; } X& operator=( X && move_semantics_are_cool ) { return this->swap(move_semantics_are_cool); } X& operator=( const X& rhs ) { X temporary_copy(rhs); return this->swap(temporary_copy); } 

    PS: Ich hoffe, ich verstehe die Bewegungssemantik nicht falsch.

    Hier ist ein tatsächliches Szenario, dem ich begegnet bin, wo dieses Idiom sehr geholfen hat. Ich habe kürzlich beschlossen, DirectX 11 sowie meine bestehende DirectX 9-Unterstützung in einer Game Engine zu unterstützen. Die Engine hat bereits die meisten DX-functionen übernommen, sodass keine der DX-Schnittstellen direkt verwendet wurde. Sie wurden nur in den Kopfzeilen als private Mitglieder definiert. Die Engine verwendet DLLs als Erweiterungen und fügt Tastatur-, Maus-, Joystick- und Skriptunterstützung hinzu, so wie viele andere Erweiterungen. Während die meisten dieser DLLs DX nicht direkt nutzten, erforderten sie Kenntnisse und Verknüpfungen zu DX, einfach weil sie Header einfügten, die DX ausstellten. Mit dem Hinzufügen von DX 11 sollte diese Komplexität dramatisch, jedoch unnötig erhöht werden. Das Verschieben der DX-Mitglieder in einen Pimpl, der nur in der Quelle definiert ist, beseitigte diese Auferlegung. Zusätzlich zu dieser Reduzierung von Bibliotheksabhängigkeiten wurden meine exponierten Schnittstellen sauberer, wenn bewegte private Elemente in die Pimpl-function übergehen und nur nach vorne gerichtete Schnittstellen freilegen.