Pimpl idiom vs Pure virtuelle Klassenschnittstelle

118

Ich habe mich gefragt, was einen Programmierer dazu bringen würde, entweder Pimpl-Idiom oder reine virtuelle Klasse und Vererbung zu wählen.

Ich verstehe, dass die Pimpl-Sprache eine explizite zusätzliche Indirektion für jede öffentliche Methode und den Aufwand für die Objekterstellung enthält.

Die virtuelle Klasse Pure hingegen verfügt über eine implizite Indirektion (vtable) für die erbende Implementierung, und ich verstehe, dass kein Aufwand für die Objekterstellung anfällt.
BEARBEITEN : Sie benötigen jedoch eine Factory, wenn Sie das Objekt von außen erstellen

Was macht die reine virtuelle Klasse weniger wünschenswert als die Pimpl-Sprache?

Arkaitz Jimenez
quelle
3
Tolle Frage, wollte nur das Gleiche fragen. Siehe auch boost.org/doc/libs/1_41_0/libs/smart_ptr/sp_techniques.html
Frank

Antworten:

60

Wenn Sie eine C ++ - Klasse schreiben, sollten Sie darüber nachdenken, ob dies der Fall sein wird

  1. Ein Werttyp

    Nach Wert kopieren, Identität ist niemals wichtig. Es ist angemessen, dass es ein Schlüssel in einer std :: map ist. Beispiel: eine "Zeichenfolge" -Klasse oder eine "Datums" -Klasse oder eine "komplexe Zahl" -Klasse. Es ist sinnvoll, Instanzen einer solchen Klasse zu "kopieren".

  2. Ein Entitätstyp

    Identität ist wichtig. Immer als Referenz übergeben, niemals als "Wert". Oft macht es keinen Sinn, Instanzen der Klasse überhaupt zu "kopieren". Wenn es sinnvoll ist, ist normalerweise eine polymorphe "Klon" -Methode besser geeignet. Beispiele: Eine Socket-Klasse, eine Datenbankklasse, eine "Richtlinien" -Klasse, alles, was in einer funktionalen Sprache ein "Abschluss" wäre.

Sowohl pImpl als auch die reine abstrakte Basisklasse sind Techniken zur Reduzierung der Abhängigkeiten von der Kompilierungszeit.

Ich verwende pImpl jedoch immer nur zum Implementieren von Werttypen (Typ 1) und nur manchmal, wenn ich Kopplungs- und Kompilierungszeitabhängigkeiten wirklich minimieren möchte. Oft ist es die Mühe nicht wert. Wie Sie zu Recht betonen, ist der syntaktische Aufwand höher, da Sie Weiterleitungsmethoden für alle öffentlichen Methoden schreiben müssen. Für Klassen vom Typ 2 verwende ich immer eine reine abstrakte Basisklasse mit zugehörigen Factory-Methoden.

Paul Hollingsworth
quelle
6
Bitte lesen Sie den Kommentar von Paul de Vrieze zu dieser Antwort . Pimpl und Pure Virtual unterscheiden sich erheblich, wenn Sie sich in einer Bibliothek befinden und Ihre .so / .dll austauschen möchten, ohne den Client neu zu erstellen. Clients verlinken namentlich auf Pimpl-Frontends, sodass es ausreicht, alte Methodensignaturen beizubehalten. OTOH in rein abstrakten Fällen verknüpfen sie effektiv über den vtable-Index, sodass das Neuordnen von Methoden oder das Einfügen in die Mitte die Kompatibilität beeinträchtigt.
SnakE
1
Sie können nur Methoden in einem Pimpl-Klassen-Frontend hinzufügen (oder neu anordnen), um die binäre Vergleichbarkeit aufrechtzuerhalten. Logischerweise haben Sie die Benutzeroberfläche immer noch geändert und es scheint ein bisschen zwielichtig. Die Antwort hier ist ein vernünftiges Gleichgewicht, das auch beim Testen von Einheiten über "Abhängigkeitsinjektion" hilfreich sein kann. Die Antwort hängt jedoch immer von den Anforderungen ab. Bibliotheksautoren von Drittanbietern (im Gegensatz zur Verwendung einer Bibliothek in Ihrer eigenen Organisation) bevorzugen möglicherweise Pimpl.
Spacen Jasset
31

Pointer to implementationIn der Regel geht es darum, strukturelle Implementierungsdetails zu verbergen. InterfacesEs geht darum, verschiedene Implementierungen zu instanziieren. Sie dienen wirklich zwei verschiedenen Zwecken.

D. Shawley
quelle
13
Nicht unbedingt, ich habe Klassen gesehen, in denen je nach gewünschter Implementierung mehrere Pickel gespeichert sind. Oft ist dies ein Win32-Impl gegenüber einem Linux-Impl von etwas, das pro Plattform unterschiedlich implementiert werden muss.
Doug T.
14
Sie können jedoch eine Schnittstelle verwenden, um die Implementierungsdetails zu entkoppeln und auszublenden
Arkaitz Jimenez,
6
Während Sie pimpl über eine Schnittstelle implementieren können, gibt es häufig keinen Grund, die Implementierungsdetails zu entkoppeln. Es gibt also keinen Grund, polymorph zu werden. Der Grund für pimpl besteht darin, Implementierungsdetails vom Client fernzuhalten (in C ++, um sie aus dem Header herauszuhalten). Sie können dies mit einer abstrakten Basis / Schnittstelle tun, aber im Allgemeinen ist dies ein unnötiger Overkill.
Michael Burr
10
Warum ist es übertrieben? Ich meine, ist die Schnittstellenmethode langsamer als die Pimpl-Methode? Es mag logische Gründe geben, aber aus praktischer Sicht würde ich sagen, dass es einfacher ist, dies mit einer abstrakten Schnittstelle zu tun
Arkaitz Jimenez
1
Ich würde sagen, abstrakte Basisklasse / Schnittstelle ist die "normale" Art, Dinge zu tun und ermöglicht einfacheres Testen durch Verspotten
Paulm
28

Die Pimpl-Sprache hilft Ihnen dabei, Build-Abhängigkeiten und -Zeiten zu reduzieren, insbesondere in großen Anwendungen, und minimiert die Header-Exposition der Implementierungsdetails Ihrer Klasse gegenüber einer Kompilierungseinheit. Die Benutzer Ihrer Klasse sollten sich nicht einmal der Existenz eines Pickels bewusst sein müssen (außer als kryptischer Zeiger, auf den sie nicht eingeweiht sind!).

Abstrakte Klassen (reine virtuelle Klassen) müssen Ihren Kunden bewusst sein: Wenn Sie versuchen, sie zu verwenden, um Kopplungen und Zirkelverweise zu reduzieren, müssen Sie eine Möglichkeit hinzufügen, die es ihnen ermöglicht, Ihre Objekte zu erstellen (z. B. über Factory-Methoden oder -Klassen). Abhängigkeitsinjektion oder andere Mechanismen).

Pontus Gagge
quelle
17

Ich suchte nach einer Antwort auf dieselbe Frage. Nachdem ich einige Artikel gelesen und geübt habe, bevorzuge ich die Verwendung von "Pure Virtual Class Interfaces" .

  1. Sie sind direkter (dies ist eine subjektive Meinung). Mit der Pimpl-Sprache habe ich das Gefühl, dass ich Code "für den Compiler" schreibe, nicht für den "nächsten Entwickler", der meinen Code liest.
  2. Einige Test-Frameworks unterstützen direkt das Verspotten von reinen virtuellen Klassen
  3. Es ist wahr, dass Sie eine Fabrik benötigen , um von außen zugänglich zu sein. Aber wenn Sie Polymorphismus nutzen möchten: Das ist auch "Pro", kein "Contra". ... und eine einfache Fabrikmethode tut nicht wirklich weh

Der einzige Nachteil ( ich versuche dies zu untersuchen ) ist, dass die Zuhälter-Sprache schneller sein könnte

  1. Wenn die Proxy-Aufrufe inline sind, muss beim Erben zur Laufzeit unbedingt ein zusätzlicher Zugriff auf das Objekt VTABLE erforderlich sein
  2. Der Speicherbedarf der pimpl public-proxy-Klasse ist geringer (Sie können problemlos Optimierungen für schnellere Swaps und ähnliche Optimierungen vornehmen).
Ilias Bartolini
quelle
21
Denken Sie auch daran, dass Sie durch die Verwendung der Vererbung eine Abhängigkeit vom vtable-Layout einführen. Um ABI aufrechtzuerhalten, können Sie die virtuellen Funktionen nicht mehr ändern (das Hinzufügen am Ende ist sicher, wenn keine untergeordneten Klassen vorhanden sind, die eigene virtuelle Methoden hinzufügen).
Paul de Vrieze
1
^ Dieser Kommentar hier sollte klebrig sein.
CodeAngry
10

Ich hasse Pickel! Sie machen den Unterricht hässlich und nicht lesbar. Alle Methoden werden auf Pickel umgeleitet. In Headern wird nie angezeigt, welche Funktionen die Klasse hat, daher können Sie sie nicht umgestalten (z. B. einfach die Sichtbarkeit einer Methode ändern). Die Klasse fühlt sich wie "schwanger" an. Ich denke, die Verwendung von Iterfaces ist besser und wirklich genug, um die Implementierung vor dem Client zu verbergen. Sie können eine Klasse mehrere Schnittstellen implementieren lassen, um sie dünn zu halten. Man sollte Schnittstellen bevorzugen! Hinweis: Sie benötigen die Factory-Klasse nicht unbedingt. Relevant ist, dass die Klassenclients über die entsprechende Schnittstelle mit ihren Instanzen kommunizieren. Das Verstecken privater Methoden finde ich als seltsame Paranoia und sehe keinen Grund dafür, da wir Schnittstellen haben.

Mascha Ananieva
quelle
1
In einigen Fällen können Sie keine reinen virtuellen Schnittstellen verwenden. Zum Beispiel, wenn Sie einen alten Code haben und zwei Module haben, die Sie trennen müssen, ohne sie zu berühren.
AlexTheo
Wie @Paul de Vrieze weiter unten ausgeführt hat, verlieren Sie beim Ändern der Methoden der Basisklasse die ABI-Kompatibilität, da Sie implizit von der vtable der Klasse abhängig sind. Es hängt vom Anwendungsfall ab, ob dies ein Problem ist.
H. Rittich
"Das Verstecken privater Methoden finde ich als seltsame Paranoia" Können Sie damit nicht die Abhängigkeiten verbergen und so die Kompilierungszeit minimieren, wenn sich eine Abhängigkeit ändert?
pooya13
Ich verstehe auch nicht, wie Fabriken leichter neu zu faktorisieren sind als pImpl. Verlassen Sie nicht in beiden Fällen die "Schnittstelle" und ändern Sie die Implementierung? In Factory müssen Sie eine .h- und eine .cpp-Datei ändern und in pImpl müssen Sie eine .h- und zwei .cpp-Dateien ändern, aber das war es auch schon, und Sie müssen im Allgemeinen die cpp-Datei der pImpl-Oberfläche nicht ändern.
pooya13
8

Es gibt ein sehr reales Problem mit gemeinsam genutzten Bibliotheken, das das Pimpl-Idiom genau umgeht, was reine Virtuals nicht können: Sie können Datenelemente einer Klasse nicht sicher ändern / entfernen, ohne Benutzer der Klasse zu zwingen, ihren Code neu zu kompilieren. Dies kann unter Umständen akzeptabel sein, z. B. nicht für Systembibliotheken.

Beachten Sie den folgenden Code in Ihrer gemeinsam genutzten Bibliothek / Ihrem Header, um das Problem im Detail zu erläutern:

// header
struct A
{
public:
  A();
  // more public interface, some of which uses the int below
private:
  int a;
};

// library 
A::A()
  : a(0)
{}

Der Compiler gibt Code in der gemeinsam genutzten Bibliothek aus, der die Adresse der zu initialisierenden Ganzzahl mit einem bestimmten Offset (in diesem Fall wahrscheinlich Null, da dies das einzige Element ist) vom Zeiger auf das ihm bekannte A-Objekt berechnet this.

Auf der Benutzerseite des Codes weist a new Azuerst sizeof(A)Speicherbytes zu und übergibt dann dem A::A()Konstruktor als einen Zeiger auf diesen Speicher alsthis .

Wenn Sie in einer späteren Version Ihrer Bibliothek die Ganzzahl löschen, vergrößern, verkleinern oder Mitglieder hinzufügen möchten, besteht eine Nichtübereinstimmung zwischen der Menge des zugewiesenen Speicherbenutzercodes und den vom Konstruktorcode erwarteten Offsets. Das wahrscheinliche Ergebnis ist ein Absturz, wenn Sie Glück haben - wenn Sie weniger Glück haben, verhält sich Ihre Software merkwürdig.

Durch Pimpl'ing können Sie Datenelemente sicher zur inneren Klasse hinzufügen und daraus entfernen, da die Speicherzuweisung und der Konstruktoraufruf in der gemeinsam genutzten Bibliothek erfolgen:

// header
struct A
{
public:
  A();
  // more public interface, all of which delegates to the impl
private:
  void * impl;
};

// library 
A::A()
  : impl(new A_impl())
{}

Alles, was Sie jetzt tun müssen, ist, Ihre öffentliche Schnittstelle frei von anderen Datenelementen als dem Zeiger auf das Implementierungsobjekt zu halten, und Sie sind vor dieser Fehlerklasse sicher.

Bearbeiten: Ich sollte vielleicht hinzufügen, dass der einzige Grund, warum ich hier über den Konstruktor spreche, darin besteht, dass ich nicht mehr Code bereitstellen wollte - die gleiche Argumentation gilt für alle Funktionen, die auf Datenelemente zugreifen.


quelle
4
Anstelle von void * halte ich es für traditioneller, die implementierende Klasse weiterzuleiten:class A_impl *impl_;
Frank Krueger
9
Ich verstehe nicht, Sie sollten keine privaten Mitglieder in einer virtuellen reinen Klasse deklarieren, die Sie als Schnittstelle verwenden möchten. Die Idee ist, die Klasse wirklich abstrakt zu halten, keine Größe, nur reine virtuelle Methoden, ich sehe nichts Sie können nicht durch gemeinsam genutzte Bibliotheken tun
Arkaitz Jimenez
@ Frank Krueger: Du hast recht, ich war faul. @Arkaitz Jimenez: Leichtes Missverständnis; Wenn Sie eine Klasse haben, die nur reine virtuelle Funktionen enthält, ist es nicht sinnvoll, über gemeinsam genutzte Bibliotheken zu sprechen. Wenn Sie jedoch mit gemeinsam genutzten Bibliotheken arbeiten, kann es aus dem oben genannten Grund ratsam sein, Ihre öffentlichen Klassen zu pimpen.
10
Das ist einfach falsch. Mit beiden Methoden können Sie den Implementierungsstatus Ihrer Klassen ausblenden, wenn Sie Ihre andere Klasse zu einer "reinen abstrakten Basis" -Klasse machen.
Paul Hollingsworth
10
Der erste Satz in Ihrem Anser impliziert, dass reine Virtuals mit einer zugehörigen Factory-Methode Sie den internen Status der Klasse irgendwie nicht verbergen lassen. Das ist nicht wahr. Mit beiden Techniken können Sie den internen Status der Klasse ausblenden. Der Unterschied besteht darin, wie es für den Benutzer aussieht. Mit pImpl können Sie weiterhin eine Klasse mit Wertsemantik darstellen und gleichzeitig den internen Status ausblenden. Mit der Factory-Methode Pure Abstract Base Class + können Sie Entitätstypen darstellen und den internen Status ausblenden. Letzteres ist genau die Funktionsweise von COM. Kapitel 1 von "Essential COM" hat diesbezüglich eine große Diskussion.
Paul Hollingsworth
6

Wir dürfen nicht vergessen, dass Vererbung eine stärkere und engere Kopplung ist als Delegation. Ich würde auch alle in den Antworten aufgeworfenen Fragen berücksichtigen, wenn ich entscheide, welche Design-Idiome zur Lösung eines bestimmten Problems verwendet werden sollen.

Sam
quelle
3

Obwohl in den anderen Antworten ausführlich darauf eingegangen, kann ich vielleicht etwas expliziter auf einen Vorteil von Pimpl gegenüber virtuellen Basisklassen eingehen:

Ein Pimpl-Ansatz ist aus Anwendersicht transparent, dh Sie können z. B. Objekte der Klasse auf dem Stapel erstellen und direkt in Containern verwenden. Wenn Sie versuchen, die Implementierung mithilfe einer abstrakten virtuellen Basisklasse auszublenden, müssen Sie einen gemeinsam genutzten Zeiger von einer Factory auf die Basisklasse zurückgeben, was die Verwendung erschwert. Betrachten Sie den folgenden äquivalenten Client-Code:

// Pimpl
Object pi_obj(10);
std::cout << pi_obj.SomeFun1();

std::vector<Object> objs;
objs.emplace_back(3);
objs.emplace_back(4);
objs.emplace_back(5);
for (auto& o : objs)
    std::cout << o.SomeFun1();

// Abstract Base Class
auto abc_obj = ObjectABC::CreateObject(20);
std::cout << abc_obj->SomeFun1();

std::vector<std::shared_ptr<ObjectABC>> objs2;
objs2.push_back(ObjectABC::CreateObject(13));
objs2.push_back(ObjectABC::CreateObject(14));
objs2.push_back(ObjectABC::CreateObject(15));
for (auto& o : objs2)
    std::cout << o->SomeFun1();
Superfly Jon
quelle
2

Nach meinem Verständnis dienen diese beiden Dinge völlig unterschiedlichen Zwecken. Der Zweck der Pickel-Redewendung besteht im Wesentlichen darin, Ihnen einen Überblick über Ihre Implementierung zu geben, damit Sie beispielsweise schnelle Swaps für eine Art durchführen können.

Der Zweck virtueller Klassen besteht eher darin, Polymorphismus zuzulassen, dh Sie haben einen unbekannten Zeiger auf ein Objekt eines abgeleiteten Typs, und wenn Sie die Funktion x aufrufen, erhalten Sie immer die richtige Funktion für jede Klasse, auf die der Basiszeiger tatsächlich zeigt.

Äpfel und Orangen wirklich.

Robert S. Barnes
quelle
Ich stimme den Äpfeln / Orangen zu. Es scheint jedoch, dass Sie pImpl für eine Funktion verwenden. Mein Ziel ist es hauptsächlich, Build-Technik und Informationen zu verstecken.
xtofl
2

Das nervigste Problem an der Pimpl-Sprache ist, dass es extrem schwierig ist, vorhandenen Code zu pflegen und zu analysieren. Wenn Sie also pimpl verwenden, zahlen Sie nur mit Entwicklerzeit und Frustration, um "Build-Abhängigkeiten und -Zeiten zu reduzieren und die Header-Exposition der Implementierungsdetails zu minimieren". Entscheide selbst, ob es sich wirklich lohnt.

Insbesondere "Build-Zeiten" sind ein Problem, das Sie durch bessere Hardware oder mithilfe von Tools wie Incredibuild (www.incredibuild.com, ebenfalls bereits in Visual Studio 2017 enthalten) lösen können, ohne Ihr Software-Design zu beeinträchtigen. Das Software-Design sollte im Allgemeinen unabhängig von der Art und Weise sein, wie die Software erstellt wird.

Trantor
quelle
Sie zahlen auch mit Entwicklerzeit, wenn die Erstellungszeiten 20 Minuten statt 2 Minuten betragen. Es ist also ein bisschen ausgewogen, ein echtes Modulsystem würde hier viel helfen.
Arkaitz Jimenez
Meiner Meinung nach sollte die Art und Weise, wie Software erstellt wird, das interne Design überhaupt nicht beeinflussen. Dies ist ein ganz anderes Problem.
Trantor
2
Was macht es schwierig zu analysieren? Eine Reihe von Aufrufen in einer Implementierungsdatei, die an die Impl-Klasse weitergeleitet wird, klingt nicht schwer.
Mabraham
2
Stellen Sie sich Debugging-Implementierungen vor, bei denen sowohl Pimpl als auch Interfaces verwendet werden. Ausgehend von einem Aufruf im Benutzercode A verfolgen Sie die Schnittstelle B, springen zur pickeligen Klasse C, um schließlich mit dem Debuggen der Implementierungsklasse D zu beginnen ... Vier Schritte, bis Sie analysieren können, was wirklich passiert. Und wenn das Ganze in einer DLL implementiert ist, finden Sie möglicherweise irgendwo dazwischen eine C-Schnittstelle ....
Trantor
Warum sollten Sie eine Schnittstelle mit pImpl verwenden, wenn pImpl auch die Aufgabe einer Schnittstelle übernehmen kann? (dh es kann Ihnen helfen, eine Abhängigkeitsinversion zu erreichen)
pooya13