wake-up-neo.com

Pimpl-Idiom gegen reine virtuelle Klasse

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 Pimpl-Idiom eine explizite zusätzliche Umleitung für jede öffentliche Methode und den Objekterstellungsaufwand beinhaltet.

Die reine virtuelle Klasse hingegen enthält implizite Indirection (vtable) für die erbende Implementierung, und ich verstehe, dass kein Objekterstellungsaufwand anfällt.
EDIT: Sie benötigen jedoch eine Fabrik, wenn Sie das Objekt von außen erstellen

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

113
Arkaitz Jimenez

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

  1. Ein Werttyp

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

  2. Ein Entitätstyp

    Identität ist wichtig. Wird immer als Referenz übergeben, niemals als "Wert". Es macht oft keinen Sinn, Instanzen der Klasse überhaupt zu "kopieren". Wenn es sinnvoll ist, ist eine polymorphe "Clone" -Methode normalerweise geeigneter. Beispiele: Eine Socket-Klasse, eine Database-Klasse, eine "Policy" -Klasse, alles, was in einer funktionalen Sprache "Schließung" wäre.

Sowohl pImpl als auch die reine abstrakte Basisklasse sind Techniken, um Abhängigkeiten beim Kompilieren zu reduzieren.

Allerdings verwende ich immer nur pImpl, um Value-Typen (Typ 1) zu implementieren, und nur manchmal, wenn ich die Abhängigkeiten zwischen Kopplung und Kompilierzeit wirklich minimieren möchte. Oft ist es die Mühe nicht wert. Sie weisen zu Recht darauf hin, dass der Syntaxaufwand höher ist, 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.

59

Bei Pointer to implementation geht es normalerweise darum, strukturelle Implementierungsdetails auszublenden. Bei Interfaces geht es darum, verschiedene Implementierungen zu erstellen. Sie dienen wirklich zwei verschiedenen Zwecken.

32
D.Shawley

Das Pimpl-Idiom hilft Ihnen dabei, Build-Abhängigkeiten und -zeiten zu reduzieren, insbesondere in großen Anwendungen, und minimiert die Header-Informationen zu den Implementierungsdetails Ihrer Klasse für eine einzige Compilationseinheit. Die Benutzer Ihrer Klasse sollten nicht einmal wissen, dass ein Pickel vorhanden ist (außer als kryptischer Zeiger, auf den sie nicht vertraulich sind!). 

Abstrakte Klassen (reine virtuelle Klassen) müssen Ihren Kunden bewusst sein: Wenn Sie versuchen, Kopplungs- und Zirkulationsreferenzen zu reduzieren, müssen Sie einen Weg hinzufügen, mit dem sie Ihre Objekte erstellen können (z. B. durch Factory-Methoden oder -Klassen). Abhängigkeitsinjektion oder andere Mechanismen). 

27
Pontus Gagge

Ich habe nach einer Antwort auf dieselbe Frage gesucht. Nach dem Lesen einiger Artikel und einiger Übung bevorzuge ich "Pure Virtual Class Interfaces".

  1. Sie sind direkter (dies ist eine subjektive Meinung). Pimpl-Idiom gibt mir das Gefühl, ich schreibe Code "für den Compiler", nicht für den "nächsten Entwickler", der meinen Code lesen wird. 
  2. Einige Testframeworks bieten direkte Unterstützung für reine virtuelle Mocking-Klassen
  3. Es ist wahr, dass Sie brauchen eine Fabrik sind, die von außen zugänglich ist. Aber wenn Sie den Polymorphismus nutzen wollen: das ist auch "Pro", kein "Betrüger". ... und eine einfache Fabrikmethode schmerzt nicht so sehr

Der einzige Nachteil (ich versuche dies zu untersuchen) ist, dass das Pimpl-Idiom schneller sein könnte

  1. wenn die Proxy-Aufrufe eingebettet sind, benötigen Sie zur Laufzeit notwendigerweise einen zusätzlichen Zugriff auf das Objekt VTABLE
  2. der Speicherbedarf der Pimpl-Public-Proxy-Klasse ist geringer (Sie können leicht Optimierungen für schnellere Swaps und andere ähnliche Optimierungen durchführen) 
13
Ilias Bartolini

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

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

// 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 es das einzige Mitglied ist) vom Zeiger auf das A-Objekt berechnet, das this ist.

Auf der Benutzerseite des Codes weist ein new A zuerst sizeof(A) Bytes des Speichers zu und gibt dann einen Zeiger auf diesen Speicher als this an den A::A()-Konstruktor.

Wenn Sie in einer späteren Revision Ihrer Bibliothek die Ganzzahl löschen, vergrößern, verkleinern oder Mitglieder hinzufügen möchten, besteht ein Konflikt zwischen der Menge des Speicherbenutzers, die vom Benutzer zugewiesen wird, und den Offsets, die der Konstruktorcode erwartet. Das wahrscheinliche Ergebnis ist ein Absturz, wenn Sie Glück haben - wenn Sie weniger Glück haben, verhält sich Ihre Software seltsam.

Durch Pimpl'ing können Sie Datenelemente sicher zur inneren Klasse hinzufügen und 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())
{}

Jetzt müssen Sie nur noch Ihre öffentliche Schnittstelle von anderen Datenmitgliedern als dem Zeiger auf das Implementierungsobjekt freigeben, und Sie sind vor dieser Fehlerklasse geschützt.

Edit: Ich sollte vielleicht hinzufügen, dass ich hier nur über den Konstruktor spreche, weil ich nicht mehr Code bereitstellen wollte - die gleiche Argumentation gilt für alle Funktionen, die auf Datenelemente zugreifen.

9
unwesen

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

8
Masha Ananieva

Wir dürfen nicht vergessen, dass Vererbung eine stärkere, engere Verbindung als Delegation darstellt. Ich würde auch alle in den Antworten angesprochenen Fragen berücksichtigen, wenn ich mich entscheide, welche Design-Idiome bei der Lösung eines bestimmten Problems zu verwenden sind.

6
Sam

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

Ein Pimpl-Ansatz ist aus Sicht der Benutzer transparent, dh Sie können z. Erstellen Sie Objekte der Klasse auf dem Stapel und verwenden Sie sie direkt in Containern. Wenn Sie versuchen, die Implementierung mithilfe einer abstrakten virtuellen Basisklasse auszublenden, müssen Sie aus einer Factory einen gemeinsamen Zeiger auf die Basisklasse zurückgeben, was die Verwendung erschwert. Betrachten Sie den folgenden äquivalenten Clientcode:

// 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();
3
Superfly Jon

In meinem Verständnis dienen diese beiden Dinge völlig unterschiedlichen Zwecken. Der Zweck des Pickel-Idioms besteht im Wesentlichen darin, Ihnen einen Überblick über Ihre Implementierung zu geben, sodass Sie zum Beispiel schnelle Sortierungen durchführen können.

Der Zweck von virtuellen Klassen ist eher die Richtung, Polymorphismus zuzulassen, d. H. Sie haben einen unbekannten Zeiger auf ein Objekt eines abgeleiteten Typs. Wenn Sie die Funktion x aufrufen, erhalten Sie immer die richtige Funktion für die Klasse, auf die der Basiszeiger tatsächlich verweist.

Äpfel und Orangen wirklich.

2

Das ärgerlichste Problem des Pimpl-Idioms ist, dass es extrem schwierig ist, vorhandenen Code zu warten und zu analysieren. Wenn Sie Pimpl verwenden, zahlen Sie nur mit Entwicklerzeit und Frustration, um "Abhängigkeiten und Buildzeiten zu reduzieren und die Header-Informationen der Implementierungsdetails zu minimieren". Entscheide dich selbst, ob es sich wirklich lohnt.

Insbesondere "Bauzeiten" ist ein Problem, das Sie durch eine bessere Hardware oder durch Verwendung von Tools wie Incredibuild (www.incredibuild.com, ebenfalls bereits in Visual Studio 2017 enthalten) lösen können, ohne dass dadurch Ihr Softwaredesign beeinträchtigt wird. Das Software-Design sollte im Allgemeinen unabhängig von der Art und Weise sein, wie die Software erstellt wird.

1
Trantor