Was ist der Sinn des PImpl-Musters, während wir das Interface in C ++ für den gleichen Zweck verwenden können?

49

Ich sehe eine Menge Quellcode, der PImpl-Idiom in C ++ verwendet. Ich gehe davon aus, dass der Zweck darin besteht, die privaten Daten / den Typ / die Implementierung zu verbergen, damit die Abhängigkeit beseitigt und die Kompilierungszeit und das Problem mit dem Header-Einschluss verringert werden können.

Interface- / pure-abstract-Klassen in C ++ verfügen jedoch auch über diese Funktion. Sie können auch zum Ausblenden von Daten / Typ / Implementierung verwendet werden. Damit der Aufrufer beim Erstellen eines Objekts nur die Schnittstelle sieht, können wir im Header der Schnittstelle eine Factory-Methode deklarieren.

Der Vergleich ist:

  1. Kosten :

    Die Kosten für den Schnittstellenweg sind geringer, da Sie die Implementierung der Public-Wrapper-Funktion nicht einmal wiederholen void Bar::doWork() { return m_impl->doWork(); }müssen. Sie müssen lediglich die Signatur in der Schnittstelle definieren.

  2. Gut verstanden :

    Die Schnittstellentechnologie wird von jedem C ++ - Entwickler besser verstanden.

  3. Leistung :

    Interface Way Performance nicht schlechter als PImpl Idiom, sowohl ein zusätzlicher Speicherzugriff. Ich gehe davon aus, dass die Leistung gleich ist.

Das Folgende ist der Pseudocode, um meine Frage zu veranschaulichen:

// Forward declaration can help you avoid include BarImpl header, and those included in BarImpl header.
class BarImpl;
class Bar
{
public:
    // public functions
    void doWork();
private:
    // You don't need to compile Bar.cpp after changing the implementation in BarImpl.cpp
    BarImpl* m_impl;
};

Der gleiche Zweck kann über die Schnittstelle implementiert werden:

// Bar.h
class IBar
{
public:
    virtual ~IBar(){}
    // public functions
    virtual void doWork() = 0;
};

// to only expose the interface instead of class name to caller
IBar* createObject();

Was ist der Sinn von PImpl?

ZijingWu
quelle

Antworten:

50

Erstens wird PImpl normalerweise für nicht polymorphe Klassen verwendet. Und wenn eine polymorphe Klasse über PImpl verfügt, bleibt sie normalerweise polymorph, implementiert weiterhin Schnittstellen und überschreibt virtuelle Methoden von der Basisklasse usw. Eine einfachere Implementierung von PImpl ist also keine Schnittstelle, sondern eine einfache Klasse, die die Mitglieder direkt enthält!

Es gibt drei Gründe, PImpl zu verwenden:

  1. Die binäre Schnittstelle (ABI) von den privaten Mitgliedern unabhängig machen. Es ist möglich, eine gemeinsam genutzte Bibliothek zu aktualisieren, ohne den abhängigen Code erneut zu kompilieren, jedoch nur, solange die Binärschnittstelle dieselbe bleibt. Nahezu jede Änderung in der Kopfzeile, mit Ausnahme des Hinzufügens einer Nichtmitgliedsfunktion und des Hinzufügens einer Nichtvirtualmitgliedsfunktion, ändert die ABI. Das PImpl-Idiom verschiebt die Definition der privaten Member in die Quelle und entkoppelt so die ABI von ihrer Definition. Siehe Fragile Binary Interface Problem

  2. Wenn sich ein Header ändert, müssen alle Quellen einschließlich des Headers neu kompiliert werden. Und die C ++ - Kompilierung ist ziemlich langsam. Durch das Verschieben von Definitionen der privaten Member in die Quelle reduziert das PImpl-Idiom die Kompilierungszeit, da weniger Abhängigkeiten in den Header gezogen werden müssen und die Kompilierungszeit nach Änderungen noch mehr verkürzt wird, da die abhängigen Member nicht neu kompiliert werden müssen (ok, das gilt auch für interface + factory function mit hidden concrete class)

  3. Für viele Klassen in C ++ ist die Sicherheit von Ausnahmen eine wichtige Eigenschaft. In vielen Fällen müssen Sie mehrere Klassen in einer zusammenfassen, damit während des Vorgangs mit mehr als einem Element kein Element geändert wird oder ein Vorgang ausgeführt wird, der das Element in einem inkonsistenten Zustand belässt, wenn er ausgelöst wird und das enthaltene Objekt verbleiben muss konsistent. In diesem Fall implementieren Sie die Operation, indem Sie eine neue Instanz von PImpl erstellen und diese bei erfolgreicher Operation austauschen.

Tatsächlich kann die Schnittstelle auch nur zum Ausblenden der Implementierung verwendet werden, hat jedoch die folgenden Nachteile:

  1. Durch das Hinzufügen einer nicht virtuellen Methode wird ABI nicht beschädigt, durch das Hinzufügen einer virtuellen Methode jedoch. Schnittstellen erlauben daher das Hinzufügen von Methoden überhaupt nicht, PImpl tut dies.

  2. Das Inteface kann nur über einen Zeiger / eine Referenz verwendet werden, sodass der Benutzer für eine ordnungsgemäße Ressourcenverwaltung sorgen muss. Andererseits sind Klassen, die PImpl verwenden, immer noch Werttypen und behandeln die Ressourcen intern.

  3. Versteckte Implementierung kann nicht vererbt werden, Klasse mit PImpl kann.

Und natürlich hilft die Benutzeroberfläche nicht bei der Ausnahmesicherheit. Dazu benötigen Sie die Indirektion innerhalb der Klasse.

Jan Hudec
quelle
Guter Punkt. Das Hinzufügen einer öffentlichen Funktion in der ImplKlasse unterbricht nicht die Funktion ABI, das Hinzufügen einer öffentlichen Funktion in der Schnittstelle wird jedoch unterbrochen ABI.
ZijingWu
1
Ist eine öffentliche Funktion / Methode Hinzufügen nicht hat das ABI zu brechen; Streng additive Änderungen sind keine wesentlichen Änderungen. Sie müssen jedoch sehr vorsichtig sein; Der Code, der zwischen dem Front-End und dem Back-End vermittelt, muss mit jeder Menge Spaß bei der Versionierung fertig werden. Es wird alles schwierig. (Aus diesem Grund bevorzugen einige Software-Projekte C gegenüber C ++. Dadurch können sie genauer feststellen, was der ABI ist.)
Donal Fellows,
3
@DonalFellows: eine öffentliche Funktion / Methode Hinzufügen nicht ABI brechen. Das Hinzufügen einer virtuellen Methode ist und bleibt immer so, es sei denn, der Benutzer kann das Objekt nicht erben (was PImpl erreicht).
Jan Hudec
4
Um zu verdeutlichen, warum das Hinzufügen einer virtuellen Methode den ABI bricht. Es hat mit Verknüpfung zu tun. Die Verknüpfung mit nicht virtuellen Methoden erfolgt namentlich, unabhängig davon, ob die Bibliothek statisch oder dynamisch ist. Durch Hinzufügen einer anderen nicht virtuellen Methode wird lediglich ein anderer Name hinzugefügt, zu dem ein Link erstellt werden soll. Virtuelle Methoden sind jedoch Indizes in einer vtable, und externer Code wird effektiv nach Index verknüpft. Durch das Hinzufügen einer virtuellen Methode können andere Methoden in der vtable verschoben werden, ohne dass der externe Code dies weiß.
SnakE
1
PImpl reduziert auch die anfängliche Kompilierungszeit. Zum Beispiel, wenn meine Implementierung ein Feld von einigen Vorlagentyp hat (zB unordered_set), ohne Pimpl, muss ich #include <unordered_set>in MyClass.h. Mit PImpl muss ich es nur in die .cppDatei aufnehmen, nicht in die .hDatei, und daher alles andere, was es beinhaltet ...
Martin C. Martin
7

Ich möchte nur Ihren Leistungspunkt ansprechen. Wenn Sie eine Schnittstelle verwenden, müssen Sie virtuelle Funktionen erstellt haben, die vom Optimierer des Compilers NICHT eingebunden werden. Die PIMPL-Funktionen können (und werden es wahrscheinlich, weil sie so kurz sind) eingebunden werden (möglicherweise mehr als einmal, wenn die IMPL-Funktion ebenfalls klein ist). Ein virtueller Funktionsaufruf kann nur optimiert werden, wenn Sie vollständige Programmanalyseoptimierungen verwenden, deren Ausführung sehr lange dauert.

Wenn Ihre PIMPL-Klasse nicht in einer leistungskritischen Weise verwendet wird, spielt dieser Punkt keine große Rolle, aber Ihre Annahme, dass die Leistung gleich ist, gilt nur in einigen Situationen, nicht in allen.

Chewy Gumball
quelle
1
Mit der Verwendung der Link-Time-Optimierung wird der größte Teil des Overheads bei der Verwendung von Pimpl-Idiomen entfernt. Sowohl die Outter-Wrapper-Funktionen als auch die Implementierungsfunktionen können integriert werden, sodass die Funktionsaufrufe wie eine normale Klasse wirken, die in einem Header implementiert ist.
Goji
Dies gilt nur, wenn Sie die Verbindungszeitoptimierung verwenden. Der Punkt von PImpl ist, dass der Compiler nicht über die Implementierung verfügt, auch nicht über die Weiterleitungsfunktion. Der Linker tut es jedoch.
Martin C. Martin
5

Diese Seite beantwortet Ihre Frage. Viele Leute stimmen Ihrer Behauptung zu. Die Verwendung von Pimpl bietet die folgenden Vorteile gegenüber privaten Feldern / Mitgliedern.

  1. Das Ändern von privaten Membervariablen einer Klasse erfordert keine Neukompilierung von Klassen, die davon abhängen, wodurch die Zeiten verkürzt werden und das FragileBinaryInterfaceProblem reduziert wird.
  2. Die Header-Datei muss keine #Klassen enthalten, die in privaten Member-Variablen 'von Wert' verwendet werden, sodass die Kompilierungszeiten kürzer sind.

Das Pimpl-Idiom ist eine Optimierung zur Kompilierungszeit, eine Technik zum Aufbrechen von Abhängigkeiten. Es werden große Header-Dateien reduziert. Es reduziert Ihren Header nur auf die öffentliche Schnittstelle. Die Header-Länge ist in C ++ wichtig, wenn Sie über die Funktionsweise nachdenken. #include verkettet die Header-Datei effektiv mit der Quelldatei. Denken Sie also daran, dass vorverarbeitete C ++ - Einheiten sehr groß sein können. Die Verwendung von Pimpl kann die Kompilierzeiten verbessern.

Es gibt andere bessere Techniken zum Aufbrechen von Abhängigkeiten, bei denen das Design verbessert wird. Was bedeuten die privaten Methoden? Was ist die einzige Verantwortung? Sollten sie eine andere Klasse sein?

Dave Hillier
quelle
PImpl ist überhaupt keine Optimierung in Bezug auf die Laufzeit. Zuweisungen sind nicht trivial und Pickel bedeutet zusätzliche Zuordnung. Es ist eine Technik, die Abhängigkeiten auflöst und nur die Kompilierzeit optimiert .
Jan Hudec
@ JanHudec geklärt.
Dave Hillier
@ DaveHillier, +1 für Erwähnung FragileBinaryInterfaceProblem
ZijingWu