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 private
Objekte von a zu erstellen class
und 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)?
quelle
struct XImpl : public X
. Das fühlt sich für mich natürlicher an. Gibt es ein anderes Problem, das ich verpasst habe?const unique_ptr<XImpl>
eher mit als implementiert werdenXImpl*
.Antworten:
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,
XImpl
ohne die Binärkompatibilität mit Ihrem Client zu beeinträchtigen (was Abstürze bedeuten würde!). Da sich das binäre Layout derX
Klasse nicht ändert, wenn Sie der Klasse neue Felder hinzufügenXimpl
, 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
/XImpl
ohne 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
X
muss, wenn Sie derXImpl
Klasse 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
X
jeder Client, der jemals zugewiesen wirdX
(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.cpp
neu kompiliert werden, auch wenn eine private MethodeX::foo()
hinzugefügtX
undX.h
geändert wurde, obwohlXClient1.cpp
diese 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.
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!
quelle
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:
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):
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-
stat
Aufruf. Hier kann die Struktur aus dem C-Header je nach#defines
Einstellung unterschiedlich sein. Und da mein Wrapper-Header nicht alle steuern kann, habe ich nur#include <sys/stat.h>
in meiner.cxx
Datei und vermeide diese Probleme.quelle
File
Klasse (die einen Großteil der Informationen enthält,stat
die unter Unix zurückgegeben werden) verwendet beispielsweise unter Windows und Unix dieselbe Benutzeroberfläche.#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.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).
quelle
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:
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.quelle
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.
quelle
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.
quelle
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:
Angenommen, es gibt nur eine Implementierung dieser Schnittstelle, ist dies klarer, wenn keine abstrakte Klasse / konkrete Implementierung verwendet wird
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.
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)
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.
(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).
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).
quelle
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,
quelle
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
quelle
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.
quelle
Ein Vorteil, den ich sehen kann, ist, dass der Programmierer bestimmte Operationen ziemlich schnell implementieren kann:
PS: Ich hoffe, ich verstehe die Bewegungssemantik nicht falsch.
quelle