Wird die pImpl-Sprache in der Praxis wirklich verwendet?

165

Ich lese das Buch "Exceptional C ++" von Herb Sutter und habe in diesem Buch etwas über die pImpl-Sprache gelernt. Grundsätzlich besteht die Idee darin, eine Struktur für die privateObjekte von a zu erstellen classund diese dynamisch zuzuweisen, um die Kompilierungszeit zu verkürzen (und auch die privaten Implementierungen besser auszublenden).

Beispielsweise:

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

könnte geändert werden zu:

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

und im CPP die Definition:

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

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

Sollte ich es überall oder mit Vorsicht verwenden? Und wird diese Technik für eingebettete Systeme empfohlen (wo die Leistung sehr wichtig ist)?

Renan Greinert
quelle
Ist dies im Wesentlichen dasselbe wie die Entscheidung, dass X eine (abstrakte) Schnittstelle und Ximpl die Implementierung ist? struct XImpl : public X. Das fühlt sich für mich natürlicher an. Gibt es ein anderes Problem, das ich verpasst habe?
Aaron McDaid
@AaronMcDaid: Es ist ähnlich, hat aber den Vorteil, dass (a) Mitgliedsfunktionen nicht virtuell sein müssen und (b) Sie keine Factory oder die Definition der Implementierungsklasse benötigen, um sie zu instanziieren.
Mike Seymour
2
@AaronMcDaid Das Pimpl-Idiom vermeidet virtuelle Funktionsaufrufe. Es ist auch ein bisschen mehr C ++ - ish (für eine Vorstellung von C ++ - ish); Sie rufen Konstruktoren anstelle von Factory-Funktionen auf. Ich habe beides verwendet, je nachdem, was in der vorhandenen Codebasis enthalten ist - das Pimpl-Idiom (ursprünglich Cheshire-Katzen-Idiom genannt und Herbs Beschreibung um mindestens 5 Jahre vorausgegangen) scheint eine längere Geschichte zu haben und mehr zu sein weit verbreitet in C ++, aber ansonsten funktionieren beide.
James Kanze
30
In C ++ sollte pimpl const unique_ptr<XImpl>eher mit als implementiert werden XImpl*.
Neil G
1
"Ich habe diesen Ansatz noch nie gesehen, weder in den Unternehmen, in denen ich gearbeitet habe, noch in Open-Source-Projekten." Qt benutzt es kaum jemals NICHT.
ManuelSchneid3r

Antworten:

132

Ich frage mich also, ob diese Technik in der Praxis wirklich angewendet wird. Sollte ich es überall oder mit Vorsicht verwenden?

Natürlich wird es verwendet. Ich benutze es in meinem Projekt, in fast jeder Klasse.


Gründe für die Verwendung der PIMPL-Sprache:

Binäre Kompatibilität

Wenn Sie eine Bibliothek entwickeln, können Sie Felder hinzufügen / ändern, XImplohne die Binärkompatibilität mit Ihrem Client zu beeinträchtigen (was Abstürze bedeuten würde!). Da sich das binäre Layout der XKlasse nicht ändert, wenn Sie der Klasse neue Felder hinzufügen Ximpl, können Sie der Bibliothek in kleineren Versionsaktualisierungen sicher neue Funktionen hinzufügen.

Natürlich können Sie auch neue öffentliche / private nicht-virtuelle Methoden hinzufügen X/ XImplohne die binäre Kompatibilität zu brechen, aber das ist auf einer Stufe mit der Standard - Header / Implementierungstechnik.

Daten verstecken

Wenn Sie eine Bibliothek entwickeln, insbesondere eine proprietäre, ist es möglicherweise wünschenswert, nicht anzugeben, welche anderen Bibliotheken / Implementierungstechniken zur Implementierung der öffentlichen Schnittstelle Ihrer Bibliothek verwendet wurden. Entweder aufgrund von Problemen mit geistigem Eigentum oder weil Sie glauben, dass Benutzer versucht sein könnten, gefährliche Annahmen über die Implementierung zu treffen, oder einfach die Kapselung durch schreckliche Casting-Tricks zu brechen. PIMPL löst / mildert das.

Kompilierungszeit

Die Kompilierungszeit wird verkürzt, da nur die Quelldatei (Implementierungsdatei) von neu erstellt werden Xmuss, wenn Sie der XImplKlasse Felder und / oder Methoden hinzufügen / entfernen (was dem Hinzufügen privater Felder / Methoden in der Standardtechnik entspricht). In der Praxis ist dies eine übliche Operation.

Bei der Standard-Header- / Implementierungstechnik (ohne PIMPL) muss beim Hinzufügen eines neuen Felds Xjeder Client, der jemals zugewiesen wird X(entweder auf einem Stapel oder auf einem Heap), neu kompiliert werden, da die Größe der Zuordnung angepasst werden muss. Nun, jeder Client, der niemals X zuweist, muss ebenfalls neu kompiliert werden, aber es ist nur Overhead (der resultierende Code auf der Clientseite ist der gleiche).

Darüber hinaus muss die Standardtrennung von Header und Implementierung XClient1.cppneu kompiliert werden, auch wenn eine private Methode X::foo()hinzugefügt Xund X.hgeändert wurde, obwohl XClient1.cppdiese Methode aus Kapselungsgründen möglicherweise nicht aufgerufen werden kann! Wie oben ist es reiner Overhead und hängt damit zusammen, wie echte C ++ - Build-Systeme funktionieren.

Natürlich ist keine Neukompilierung erforderlich, wenn Sie nur die Implementierung der Methoden ändern (weil Sie den Header nicht berühren), aber dies entspricht der Standard-Header- / Implementierungstechnik.


Wird diese Technik für eingebettete Systeme empfohlen (bei denen die Leistung sehr wichtig ist)?

Das hängt davon ab, wie mächtig Ihr Ziel ist. Die einzige Antwort auf diese Frage lautet jedoch: Messen und bewerten Sie, was Sie gewinnen und verlieren. Beachten Sie außerdem, dass nur der Vorteil der Kompilierungszeit gilt, wenn Sie keine Bibliothek veröffentlichen, die von Ihren Clients in eingebetteten Systemen verwendet werden soll!

BЈовић
quelle
16
+1, weil es in der Firma, für die ich arbeite, und aus den gleichen Gründen weit verbreitet ist.
Benoit
9
auch binäre Kompatibilität
Ambroz Bizjak
9
In der Qt-Bibliothek wird diese Methode auch in Situationen mit intelligenten Zeigern verwendet. Daher behält QString seinen Inhalt intern als unveränderliche Klasse bei. Wenn die öffentliche Klasse "kopiert" wird, wird der Zeiger des privaten Mitglieds anstelle der gesamten privaten Klasse kopiert. Diese privaten Klassen verwenden dann auch intelligente Zeiger, sodass Sie mit den meisten Klassen im Grunde eine Speicherbereinigung erhalten, zusätzlich zu einer stark verbesserten Leistung aufgrund des Zeigerkopierens anstelle des vollständigen Klassenkopierens
Timothy Baldridge
8
Darüber hinaus kann Qt mit der Pimpl-Sprache sowohl die Vorwärts- als auch die Rückwärts-Binärkompatibilität innerhalb einer einzigen Hauptversion (in den meisten Fällen) aufrechterhalten. IMO ist dies bei weitem der wichtigste Grund, es zu verwenden.
Whitequark
1
Es ist auch nützlich für die Implementierung von plattformspezifischem Code, da Sie dieselbe API beibehalten können.
Doc
49

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

Aber wie bei allen Dingen sollten Sie niemals überall etwas ohne Vorsicht verwenden. Denken Sie immer nach, bevor Sie es verwenden. Bewerten Sie, welche Vorteile es Ihnen bietet und ob sie den von Ihnen gezahlten Preis wert sind.

Die Vorteile es kann Ihnen sind:

  • hilft bei der Aufrechterhaltung der Binärkompatibilität von gemeinsam genutzten Bibliotheken
  • bestimmte interne Details verstecken
  • abnehmende Rekompilierungszyklen

Dies können echte Vorteile für Sie sein oder auch nicht. Wie bei mir sind mir ein paar Minuten Neukompilierungszeit egal. Endbenutzer tun dies normalerweise auch nicht, da sie es immer einmal und von Anfang an kompilieren.

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

  • Erhöhung der Speichernutzung aufgrund von mehr Zuordnungen als bei der naiven Variante
  • erhöhter Wartungsaufwand (Sie müssen mindestens die Weiterleitungsfunktionen schreiben)
  • Leistungsverlust (der Compiler ist möglicherweise nicht in der Lage, Inhalte zu integrieren, wie dies bei einer naiven Implementierung Ihrer Klasse der Fall ist).

Geben Sie also sorgfältig einen Wert und bewerten Sie ihn selbst. Für mich stellt sich fast immer heraus, dass sich die Verwendung der Pimpl-Sprache nicht lohnt. Es gibt nur einen Fall, in dem ich es persönlich benutze (oder zumindest etwas Ähnliches):

Mein C ++ - Wrapper für den Linux- statAufruf. Hier kann die Struktur aus dem C-Header je nach #definesEinstellung unterschiedlich sein. Und da mein Wrapper-Header nicht alle steuern kann, habe ich nur #include <sys/stat.h>in meiner .cxxDatei und vermeide diese Probleme.

PlasmaHH
quelle
2
Es sollte fast immer für Systemschnittstellen verwendet werden, um das Schnittstellencode-System unabhängig zu machen. Meine FileKlasse (die einen Großteil der Informationen enthält, statdie unter Unix zurückgegeben werden) verwendet beispielsweise unter Windows und Unix dieselbe Benutzeroberfläche.
James Kanze
5
@JamesKanze: Selbst dort würde ich persönlich zuerst einen Moment sitzen und darüber nachdenken, ob es vielleicht nicht ausreicht, ein paar Sekunden zu haben #ifdef, um die Hülle so dünn wie möglich zu machen. Aber jeder hat andere Ziele. Wichtig ist, dass Sie sich die Zeit nehmen, darüber nachzudenken, anstatt blindlings etwas zu verfolgen.
PlasmaHH
31

Stimmen Sie mit allen anderen über die Waren überein, aber lassen Sie mich eine Grenze setzen: funktioniert nicht gut mit Vorlagen .

Der Grund dafür ist, dass für die Vorlageninstanziierung die vollständige Deklaration erforderlich ist, die dort verfügbar ist, wo die Instanziierung stattgefunden hat. (Und das ist der Hauptgrund, warum in CPP-Dateien keine Vorlagenmethoden definiert sind.)

Sie können weiterhin auf Vorlagenunterklassen verweisen, aber da Sie alle einbeziehen müssen, geht jeder Vorteil der "Implementierungsentkopplung" beim Kompilieren (Vermeidung, dass der gesamte platoformspezifische Code überall enthalten ist, Verkürzung der Kompilierung) verloren.

Ist ein gutes Paradigma für klassisches OOP (vererbungsbasiert), aber nicht für generische Programmierung (spezialisierungsbasiert).

Emilio Garavaglia
quelle
4
Sie haben genauer zu sein: es gibt absolut kein Problem , wenn mit PIMPL Klassen als Vorlagentyp Argument. Nur wenn die Implementierungsklasse selbst anhand der Vorlagenargumente der äußeren Klasse parametrisiert werden muss, kann sie nicht mehr vor dem Schnittstellenheader ausgeblendet werden, selbst wenn es sich noch um eine private Klasse handelt. Wenn Sie das Vorlagenargument löschen können, können Sie mit Sicherheit immer noch "richtig" PIMPL ausführen. Beim Löschen von Typen können Sie die PIMPL auch in einer Basisklasse ohne Vorlage ausführen und dann die Vorlagenklasse daraus ableiten lassen.
Setzen Sie Monica
22

Andere Leute haben bereits die technischen Vor- und Nachteile angegeben, aber ich denke, Folgendes ist erwähnenswert:

Sei in erster Linie nicht dogmatisch. Wenn pImpl für Ihre Situation geeignet ist, verwenden Sie es - verwenden Sie es nicht nur, weil "es besser ist, weil es die Implementierung wirklich verbirgt" usw. Zitieren der C ++ - FAQ:

Die Kapselung gilt für Code, nicht für Personen ( Quelle ).

Nur um Ihnen ein Beispiel für Open Source-Software zu geben, wo sie verwendet wird und warum: OpenThreads, die vom OpenSceneGraph verwendete Threading-Bibliothek . Die Hauptidee besteht darin, den <Thread.h>gesamten plattformspezifischen Code (z. B. ) aus dem Header zu entfernen , da sich interne Statusvariablen (z. B. Thread-Handles) von Plattform zu Plattform unterscheiden. Auf diese Weise kann man Code für Ihre Bibliothek kompilieren, ohne die Besonderheiten der anderen Plattformen zu kennen, da alles verborgen ist.

Azalee
quelle
12

Ich würde PIMPL hauptsächlich für Klassen in Betracht ziehen, die als API von anderen Modulen verwendet werden. Dies hat viele Vorteile, da die Neukompilierung der in der PIMPL-Implementierung vorgenommenen Änderungen keinen Einfluss auf den Rest des Projekts hat. Außerdem fördern sie für API-Klassen 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 dieselbe Binärschnittstelle hat - die von der PIMPL bereitgestellte Schnittstelle).

Bei der Verwendung von PIMPL für jede Klasse würde ich Vorsicht walten lassen, da all diese Vorteile mit Kosten verbunden sind: Für den Zugriff auf die Implementierungsmethoden ist eine zusätzliche Indirektionsebene erforderlich.

Ghita
quelle
"Für den Zugriff auf die Implementierungsmethoden ist eine zusätzliche Indirektionsebene erforderlich." Es ist?
Xaxxon
@xxxon ja, das ist es. pimpl ist langsamer, wenn die Methoden niedrig sind. Verwenden Sie es zum Beispiel niemals für Dinge, die in einer engen Schleife leben.
Erik Aronesty
@xaxxon Ich würde im Allgemeinen sagen, dass ein zusätzliches Level erforderlich ist. Wenn Inlining durchgeführt wird, als nein. Inlinning wäre jedoch keine Option in Code, der in einer anderen DLL kompiliert wurde.
Ghita
5

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

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

Der besondere Zweck dieses Idoims in unserem Projekt bestand darin, die von der XImpl-Klasse verwendeten Typen auszublenden. Insbesondere haben wir es verwendet, um Details von Implementierungen für unterschiedliche Hardware auszublenden, bei denen unterschiedliche Header abgerufen wurden. Wir hatten unterschiedliche Implementierungen von XImpl-Klassen für eine Plattform und unterschiedliche für die andere. Das Layout der Klasse X blieb unabhängig von der Plattform gleich.

user377178
quelle
4

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

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

Die Vorteile von pImpl sind:

  1. Angenommen, es gibt nur eine Implementierung dieser Schnittstelle, ist dies klarer, wenn keine abstrakte Klasse / konkrete Implementierung verwendet wird

  2. Wenn Sie über eine Reihe von Klassen (ein Modul) verfügen, sodass mehrere Klassen auf dasselbe "impl" zugreifen, Benutzer des Moduls jedoch nur die "exponierten" Klassen verwenden.

  3. Keine V-Tabelle, wenn davon ausgegangen wird, dass dies eine schlechte Sache ist.

Die Nachteile, die ich bei pImpl festgestellt habe (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" -Implementierung erstellen, die beim Testen von Einheiten funktioniert.

  2. (Das größte Problem). Vor den Tagen von unique_ptr und Verschieben hatten Sie eingeschränkte Auswahlmöglichkeiten zum Speichern des pImpl. Ein roher Zeiger und Sie hatten Probleme damit, dass Ihre Klasse nicht kopierbar ist. Ein altes auto_ptr würde nicht mit vorwärts deklarierter Klasse funktionieren (sowieso nicht auf allen Compilern). Die Leute haben also angefangen, shared_ptr zu verwenden, was gut war, um Ihre Klasse kopierbar zu machen, aber natürlich hatten beide Kopien das gleiche zugrunde liegende shared_ptr, das Sie möglicherweise nicht erwarten (ändern Sie eine und beide werden geändert). Daher bestand die Lösung häufig darin, einen rohen Zeiger für den inneren zu verwenden und die Klasse nicht kopierbar zu machen und stattdessen einen shared_ptr zurückzugeben. Also zwei Anrufe zu neu. (Tatsächlich haben 3 mit altem shared_ptr einen zweiten gegeben).

  3. Technisch nicht wirklich const-korrekt, da die Konstanz nicht an einen Member-Zeiger weitergegeben wird.

Im Allgemeinen bin ich daher in den letzten Jahren von pImpl weggegangen und habe stattdessen abstrakte Schnittstellen verwendet (und Factory-Methoden zum Erstellen von Instanzen).

Goldesel
quelle
3

Wie viele andere sagten, ermöglicht das Pimpl-Idiom das Erreichen einer vollständigen Unabhängigkeit beim Ausblenden und Kompilieren von Informationen, leider mit den Kosten für Leistungsverlust (zusätzliche Zeiger-Indirektion) und zusätzlichem Speicherbedarf (der Mitgliedszeiger selbst). Die zusätzlichen Kosten können bei der Entwicklung eingebetteter Software von entscheidender Bedeutung sein, insbesondere in solchen Szenarien, in denen der Speicher so weit wie möglich gespart werden muss. Die Verwendung von abstrakten C ++ - Klassen als Schnittstellen würde zu denselben Vorteilen bei gleichen Kosten führen. Dies zeigt tatsächlich einen großen Mangel an C ++, bei dem es ohne Wiederholung von C-ähnlichen Schnittstellen (globale Methoden mit einem undurchsichtigen Zeiger als Parameter) nicht möglich ist, echte Informationsverstecke und Kompilierungsunabhängigkeit ohne zusätzliche Ressourcennachteile zu erreichen. Dies liegt hauptsächlich daran, dass Deklaration einer Klasse, die von ihren Benutzern aufgenommen werden muss,

ncsc
quelle
3

Hier ist ein aktuelles Szenario, in dem diese Redewendung sehr hilfreich war. Ich habe kürzlich beschlossen, DirectX 11 sowie meine vorhandene DirectX 9-Unterstützung in einer Spiel-Engine zu unterstützen. Die Engine hat bereits die meisten DX-Funktionen enthalten, 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, ebenso wie viele andere Erweiterungen pro Woche. Während die meisten dieser DLLs DX nicht direkt verwendeten, erforderten sie Kenntnisse und eine Verknüpfung mit DX, nur weil sie Header eingezogen hatten, die DX verfügbar machten. Durch das Hinzufügen von DX 11 sollte diese Komplexität dramatisch, jedoch unnötig zunehmen. Durch das Verschieben der DX-Elemente in einen Pimpl, der nur in der Quelle definiert ist, wurde diese Auferlegung beseitigt. Zusätzlich zu dieser Reduzierung der Bibliotheksabhängigkeiten

Kit10
quelle
2

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

Dies ist eine gute Idee, aber es gibt einen echten Nachteil: Debuggen Solange der in privaten Implementierungen versteckte Code von höchster Qualität ist, ist alles in Ordnung, aber wenn es Fehler gibt, hat der Benutzer / Entwickler ein Problem. weil es nur ein dummer Zeiger auf eine versteckte Implementierung ist, selbst wenn er den Quellcode der Implementierung hat.

So wie bei fast allen Designentscheidungen gibt es Vor- und Nachteile.

Holger Kretzschmar
quelle
9
Es ist dumm, aber es ist getippt ... warum können Sie dem Code im Debugger nicht folgen?
Onkel Zeiv
2
Im Allgemeinen müssen Sie Qt selbst erstellen, um in Qt-Code zu debuggen. Sobald Sie dies getan haben, ist es kein Problem, in PIMPL-Methoden einzusteigen und den Inhalt der PIMPL-Daten zu überprüfen.
Stellen Sie Monica am
0

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.

BenGoldberg
quelle