Schnittstellenklassen werden in C ++ - Programmen sehr häufig verwendet. Leider werden bei der Implementierung von Lösungen, die auf Schnittstellenklassen basieren, häufig Fehler gemacht. Der Artikel beschreibt das korrekte Entwerfen von Schnittstellenklassen. Dabei werden verschiedene Optionen berücksichtigt. Die Verwendung von intelligenten Zeigern wird ausführlich beschrieben. Es wird ein Beispiel für die Implementierung einer Ausnahmeklasse und einer Sammlungsklassenvorlage basierend auf Schnittstellenklassen gegeben.
Inhaltsverzeichnis
Einführung
Eine Schnittstellenklasse ist eine Klasse, die keine Daten enthält und hauptsächlich aus rein virtuellen Funktionen besteht. Mit dieser Lösung können Sie die Implementierung vollständig von der Schnittstelle trennen - der Client verwendet die Schnittstellenklasse - und an einer anderen Stelle wird eine abgeleitete Klasse erstellt, in der rein virtuelle Funktionen neu definiert und die Factory-Funktion definiert werden. Implementierungsdetails sind dem Client vollständig verborgen. Auf diese Weise wird eine echte Kapselung implementiert, was mit der üblichen Klasse unmöglich ist. Sie können über Schnittstellenklassen von Scott Meyers [Meyers2] lesen. Schnittstellenklassen werden auch als Protokollklassen bezeichnet.
Durch die Verwendung von Schnittstellenklassen können Sie die Abhängigkeiten zwischen verschiedenen Teilen des Projekts schwächen, was die Teamentwicklung vereinfacht und die Kompilierungs- / Montagezeit verkürzt. Schnittstellenklassen erleichtern die Implementierung flexibler, dynamischer Lösungen, wenn Module zur Laufzeit selektiv geladen werden. Die Verwendung der Schnittstellenklassen als Schnittstellenbibliothek (API) (SDK) vereinfacht die Lösung von Binärkompatibilitätsproblemen.
Schnittstellenklassen sind weit verbreitet und implementieren mit ihrer Hilfe die Schnittstelle (API) von Bibliotheken (SDK), die Schnittstelle von Plug-Ins (Plugins) und vieles mehr. Viele Gang of Four [GoF] -Muster werden natürlich mithilfe von Schnittstellenklassen implementiert. Schnittstellenklassen umfassen COM-Schnittstellen. Leider werden bei der Implementierung von Lösungen, die auf Schnittstellenklassen basieren, häufig Fehler gemacht. Versuchen wir, dieses Problem zu klären.
1. Spezielle Elementfunktionen zum Erstellen und Löschen von Objekten
In diesem Abschnitt werden einige C ++ - Funktionen kurz beschrieben, die Sie kennen müssen, um die für Schnittstellenklassen angebotenen Lösungen vollständig zu verstehen.
1.1. Spezielle Mitgliederfunktionen
Wenn der Programmierer die Elementfunktionen der Klasse aus der folgenden Liste nicht definiert hat - Standardkonstruktor, Kopierkonstruktor, Kopierzuweisungsoperator, Destruktor -, kann der Compiler dies für ihn tun. C ++ 11 hat dieser Liste einen Verschiebungskonstruktor und einen Verschiebungszuweisungsoperator hinzugefügt. Diese Elementfunktionen werden als spezielle Elementfunktionen bezeichnet. Sie werden nur generiert, wenn sie verwendet werden, und zusätzliche Bedingungen, die für jede Funktion spezifisch sind, sind erfüllt. Wir machen darauf aufmerksam, dass sich diese Verwendung als ziemlich versteckt herausstellen kann (zum Beispiel bei der Implementierung der Vererbung). Wenn die erforderliche Funktion nicht generiert werden kann, wird ein Fehler generiert. (Mit Ausnahme von Verschiebungsvorgängen werden sie durch Kopiervorgänge ersetzt.) Die vom Compiler generierten Elementfunktionen sind öffentlich und können eingebettet werden.
Spezielle Elementfunktionen werden nicht vererbt. Wenn in der abgeleiteten Klasse eine spezielle Elementfunktion erforderlich ist, versucht der Compiler immer, diese zu generieren. Das Vorhandensein der entsprechenden Elementfunktion, die vom Programmierer in der Basisklasse definiert wurde, hat keinen Einfluss darauf.
Der Programmierer kann die Generierung spezieller Elementfunktionen verbieten. In C ++ 11 muss beim Deklarieren das Konstrukt "=delete"
verwendet werden. In C ++ 98 muss die entsprechende Elementfunktion als privat deklariert und nicht definiert werden. Bei der Klassenvererbung gilt das Verbot, eine spezielle Elementfunktion in der Basisklasse zu generieren, für alle abgeleiteten Klassen.
Wenn der Programmierer mit den vom Compiler generierten Elementfunktionen vertraut ist, kann er dies in C ++ 11 explizit angeben und nicht nur die Deklaration löschen. Dazu müssen Sie beim Deklarieren das Konstrukt "=default"
verwenden, während der Code besser gelesen wird und zusätzliche Funktionen im Zusammenhang mit der Verwaltung der Zugriffsebene "=default"
werden.
Details zu speziellen Mitgliedsfunktionen finden Sie in [Meyers3].
1.2. Objekte erstellen und löschen - grundlegende Details
Das Erstellen und Löschen von Objekten mit den Operatoren new/delete
ist eine typische Zwei-in-Eins-Operation. Beim Aufruf von new
wird zuerst Speicher für das Objekt zugewiesen. Wenn die Auswahl erfolgreich ist, wird der Konstruktor aufgerufen. Wenn der Konstruktor eine Ausnahme auslöst, wird der zugewiesene Speicher freigegeben. Wenn der delete
aufgerufen wird, geschieht alles in umgekehrter Reihenfolge: Zuerst wird der Destruktor aufgerufen, dann wird Speicher freigegeben. Der Destruktor sollte keine Ausnahmen auslösen.
Wenn der new
Operator zum Erstellen eines Array von Objekten verwendet wird, wird zunächst Speicher für das gesamte Array zugewiesen. Wenn die Auswahl erfolgreich ist, wird der Standardkonstruktor für jedes Element des Arrays ab Null aufgerufen. Wenn ein Konstruktor eine Ausnahme auslöst, wird für alle erstellten Elemente des Arrays der Destruktor in der umgekehrten Reihenfolge des Konstruktoraufrufs aufgerufen, und der zugewiesene Speicher wird freigegeben. Um ein Array zu löschen, müssen Sie den Operator delete[]
(als delete
für Arrays bezeichnet) aufrufen. Für alle Elemente des Arrays wird der Destruktor in umgekehrter Reihenfolge wie der Konstruktoraufruf aufgerufen. Anschließend wird der zugewiesene Speicher freigegeben.
Achtung! Sie müssen die richtige Form des delete
aufrufen, je nachdem, ob ein einzelnes Objekt oder Array gelöscht wird. Diese Regel muss strikt eingehalten werden, sonst kann es zu undefiniertem Verhalten kommen, dh es kann alles passieren: Speicherlecks, Absturz usw. Siehe [Meyers2] für Details.
Standardspeicherzuweisungsfunktionen erfüllen die Anforderung nicht und std::bad_alloc
eine Ausnahme vom Typ std::bad_alloc
.
Es ist sicher, eine beliebige Form des delete
auf einen Nullzeiger anzuwenden.
In der obigen Beschreibung ist eine Klarstellung erforderlich. Für die sogenannten trivialen Typen (eingebaute Typen, Strukturen im C-Stil) darf der Konstruktor nicht aufgerufen werden, und der Destruktor tut auf keinen Fall etwas. Siehe auch Abschnitt 1.6.
1.3. Destruktorzugriffsebene
Wenn der delete
auf einen Zeiger auf eine Klasse angewendet wird, muss der Destruktor dieser Klasse am delete
verfügbar sein. (Es gibt eine Ausnahme von dieser Regel, die in Abschnitt 1.6 erläutert wird.) Indem der Destruktor sicher oder geschlossen wird, verbietet der Programmierer die Verwendung des delete
, wenn der Destruktor nicht verfügbar ist. Denken Sie daran, dass der Compiler dies selbst tut, wenn in der Klasse kein Destruktor definiert ist, und dieser Destruktor geöffnet ist (siehe Abschnitt 1.1).
1.4. In einem Modul erstellen und löschen
Wenn der new
Operator ein Objekt erstellt hat, muss sich der delete
im selben Modul befinden, delete
es zu delete
. Im übertragenen Sinne: "Platziere es dort, wo du es genommen hast." Diese Regel ist bekannt, siehe zum Beispiel [Sutter / Alexandrescu]. Wenn diese Regel verletzt wird, kann es zu einer „Nichtübereinstimmung“ der Funktionen zum Zuweisen und Freigeben von Speicher kommen, was in der Regel zum Absturz des Programms führt.
1.5. Polymorphe Deletion
Wenn Sie eine polymorphe Hierarchie von Klassen entwerfen, deren Instanzen mit dem delete
gelöscht werden, muss in der Basisklasse ein offener virtueller Destruktor vorhanden sein. Dadurch wird sichergestellt, dass der Destruktor des tatsächlichen Objekttyps aufgerufen wird, wenn der delete
auf den Zeiger auf die Basisklasse angewendet wird. Wenn diese Regel verletzt wird, kann ein Aufruf des Basisklassen-Destruktors erfolgen, der zu einem Ressourcenleck führen kann.
1.6. Löschen, wenn die Klassendeklaration unvollständig ist
Die Allesfresserhaftigkeit des delete
kann bestimmte Probleme verursachen, sie kann auf einen Zeiger vom Typ void*
oder auf einen Zeiger auf eine Klasse angewendet werden, die eine unvollständige (präemptive) Deklaration aufweist. In diesem Fall tritt kein Fehler auf, nur der Aufruf des Destruktors wird übersprungen, nur die Funktion zum Freigeben des Speichers wird aufgerufen. Betrachten Sie ein Beispiel:
class X;
Dieser Code wird auch dann kompiliert, wenn beim delete
Dial Peer keine vollständige X
Klassendeklaration verfügbar ist. Richtig, beim Kompilieren (Visual Studio) wird eine Warnung ausgegeben:
warning C4150: deletion of pointer to incomplete type 'X'; no destructor called
Wenn es eine Implementierung von X
und CreateX()
, wird der Code CreateX()
. Wenn CreateX()
einen Zeiger auf das vom new
Operator erstellte Objekt zurückgibt, wird der Aufruf Foo()
erfolgreich ausgeführt, der Destruktor wird nicht aufgerufen. Es ist klar, dass dies zu einem Ressourcenverbrauch führen kann, also noch einmal über die Notwendigkeit, vorsichtig mit Warnungen umzugehen.
Diese Situation ist nicht weit hergeholt, sie kann leicht auftreten, wenn Klassen wie Smart Pointer oder Deskriptorklassen verwendet werden. Scott Meyers befasst sich mit diesem Thema in [Meyers3].
2. Rein virtuelle Funktionen und abstrakte Klassen
Das Konzept der Schnittstellenklassen basiert auf C ++ - Konzepten wie reinen virtuellen Funktionen und abstrakten Klassen.
2.1. Reine virtuelle Funktionen
Eine mit dem Konstrukt "=0"
deklarierte virtuelle Funktion wird als rein virtuell bezeichnet.
class X {
Im Gegensatz zu einer regulären virtuellen Funktion kann eine rein virtuelle Funktion nicht definiert werden (mit Ausnahme des Destruktors, siehe Abschnitt 2.3), sondern muss in einer der abgeleiteten Klassen neu definiert werden.
Es können rein virtuelle Funktionen definiert werden. Emblem Sutter bietet verschiedene nützliche Anwendungen für diese Funktion [Shutter].
2.2. Abstrakte Klassen
Eine abstrakte Klasse ist eine Klasse, die mindestens eine rein virtuelle Funktion hat. Eine Klasse, die von einer abstrakten Klasse abgeleitet ist und mindestens eine rein virtuelle Funktion nicht überschreibt, ist ebenfalls abstrakt. Der C ++ - Standard verbietet das Erstellen von Instanzen einer abstrakten Klasse. Sie können nur Instanzen von Ableitungen nicht abstrakter Klassen erstellen. Somit wird eine abstrakte Klasse erstellt, die als Basisklasse verwendet werden soll. Wenn ein Konstruktor in einer abstrakten Klasse definiert ist, ist es dementsprechend nicht sinnvoll, ihn zu öffnen, sondern muss geschützt werden.
2.3. Reiner virtueller Destruktor
In einigen Fällen ist es ratsam, einen reinen virtuellen Destruktor zu erstellen. Diese Lösung hat jedoch zwei Funktionen.
- Ein rein virtueller Destruktor muss definiert werden. (Die Standarddefinition wird normalerweise verwendet, dh mit dem Konstrukt
"=default"
.) Der abgeleitete Klassendestruktor ruft Basisklassendestruktoren entlang der gesamten Vererbungskette auf, und daher erreicht die Warteschlange garantiert den Stamm - einen rein virtuellen Destruktor. - Wenn der Programmierer keinen reinen virtuellen Destruktor in der abgeleiteten Klasse neu definiert hat, erledigt der Compiler dies für ihn (siehe Abschnitt 1.1). Somit kann eine Klasse, die von einer abstrakten Klasse mit einem rein virtuellen Destruktor abgeleitet ist, ihre Abstraktheit verlieren, ohne den Destruktor explizit zu überschreiben.
Ein Beispiel für die Verwendung eines reinen virtuellen Destruktors finden Sie in Abschnitt 4.4.
3. Schnittstellenklassen
Eine Schnittstellenklasse ist eine abstrakte Klasse, die keine Daten enthält und hauptsächlich aus rein virtuellen Funktionen besteht. Eine solche Klasse kann gewöhnliche virtuelle Funktionen (nicht rein virtuell) haben, beispielsweise einen Destruktor. Es können auch statische Elementfunktionen vorhanden sein, z. B. Werksfunktionen.
3.1. Implementierungen
Eine Implementierung einer Schnittstellenklasse wird als abgeleitete Klasse bezeichnet, in der rein virtuelle Funktionen neu definiert werden. Es können mehrere Implementierungen derselben Schnittstellenklasse vorhanden sein, und zwei Schemata sind möglich: horizontal, wenn mehrere verschiedene Klassen dieselbe Schnittstellenklasse erben, und vertikal, wenn die Schnittstellenklasse die Wurzel der polymorphen Hierarchie ist. Natürlich kann es Hybriden geben.
Der Kernpunkt des Konzepts der Schnittstellenklassen ist die vollständige Trennung der Schnittstelle von der Implementierung - der Client arbeitet nur mit der Schnittstellenklasse, die Implementierung steht ihr nicht zur Verfügung.
3.2. Objekterstellung
Die Unzugänglichkeit der Implementierungsklasse verursacht bestimmte Probleme beim Erstellen von Objekten. Der Client muss eine Instanz der Implementierungsklasse erstellen und einen Zeiger auf die Schnittstellenklasse erhalten, über die auf das Objekt zugegriffen wird. Da die Implementierungsklasse nicht verfügbar ist, können Sie den Konstruktor nicht verwenden. Daher wird die Factory-Funktion verwendet, die auf der Implementierungsseite definiert ist. Diese Funktion erstellt normalerweise ein Objekt mit dem new
Operator und gibt einen Zeiger auf das erstellte Objekt zurück, der in einen Zeiger auf eine Schnittstellenklasse umgewandelt wird. Eine Factory-Funktion kann ein statisches Mitglied einer Schnittstellenklasse sein, ist jedoch nicht erforderlich. Sie kann beispielsweise Mitglied einer speziellen Factory-Klasse (die wiederum selbst eine Schnittstellenklasse sein kann) oder eine freie Funktion sein. Eine Factory-Funktion kann keinen Rohzeiger auf eine Schnittstellenklasse zurückgeben, sondern einen intelligenten. Diese Option wird in den Abschnitten 3.3.4 und 4.3.2 erläutert.
3.3. Objekt löschen
Das Entfernen eines Objekts ist ein äußerst kritischer Vorgang. Ein Fehler führt entweder zu einem Speicherverlust oder zu einem doppelten Löschen, was normalerweise zu einem Programmabsturz führt. Im Folgenden wird dieses Problem als so detailliert wie möglich betrachtet, wobei der Vermeidung fehlerhafter Kundenaktionen große Aufmerksamkeit gewidmet wird.
Es gibt vier Hauptoptionen:
- Verwenden des
delete
. - Verwendung einer speziellen virtuellen Funktion.
- Verwendung einer externen Funktion.
- Automatisches Löschen mit Smart Pointer.
3.3.1. Verwenden des delete
Dazu müssen Sie einen offenen virtuellen Destruktor in der Schnittstellenklasse haben. In diesem Fall ruft der delete
, der auf der Clientseite einen Zeiger auf eine Schnittstellenklasse aufruft, den Destruktor der Implementierungsklasse auf. Diese Option funktioniert möglicherweise, ist jedoch schwer als erfolgreich zu erkennen. Wir erhalten Aufrufe der new
und delete
auf verschiedenen Seiten der "Barriere", new
auf der Implementierungsseite, delete
auf der Clientseite. Und wenn die Implementierung der Schnittstellenklasse in einem separaten Modul erfolgt (was durchaus üblich ist), erhalten wir einen Verstoß gegen die Regel aus Abschnitt 1.4.
3.3.2. Verwendung einer speziellen virtuellen Funktion
Progressiver ist eine weitere Option: Die Schnittstellenklasse muss über eine spezielle virtuelle Funktion verfügen, mit der das Objekt entfernt wird. Eine solche Funktion läuft letztendlich darauf hinaus, delete this
aufzurufen, aber dies geschieht bereits auf der Implementierungsseite. Eine solche Funktion kann auf verschiedene Arten aufgerufen werden, z. B. Delete()
, es werden jedoch auch andere Optionen verwendet: Release()
, Destroy()
, Dispose()
, Free()
, Close()
usw. Neben der Befolgung der Regel in Abschnitt 1.4 bietet diese Option mehrere zusätzliche Vorteile.
- Ermöglicht die Verwendung benutzerdefinierter Speicherzuweisungs- / Freigabefunktionen für die Implementierungsklasse.
- Ermöglicht die Implementierung eines komplexeren Schemas zur Steuerung der Lebensdauer des Implementierungsobjekts, z. B. mithilfe eines Referenzzählers.
In dieser Ausführungsform kann ein Versuch, ein Objekt unter Verwendung des delete
zu löschen, kompiliert und sogar ausgeführt werden, dies ist jedoch ein Fehler. Um dies in der Schnittstellenklasse zu verhindern, reicht ein leerer oder rein virtuell geschützter Destruktor aus (siehe Abschnitt 1.3). Beachten Sie, dass die Verwendung des delete
sehr maskiert sein kann. Beispielsweise verwenden standardmäßige intelligente Zeiger den delete
, um ein Objekt standardmäßig zu löschen, und der entsprechende Code ist tief in ihrer Implementierung vergraben. Mit einem geschützten Destruktor können Sie alle derartigen Versuche in der Kompilierungsphase erkennen.
3.3.3. Verwendung einer externen Funktion
Diese Option kann eine gewisse Symmetrie von Prozeduren zum Erstellen und Löschen eines Objekts aufweisen, hat jedoch in Wirklichkeit keine Vorteile gegenüber der vorherigen Version, es gibt jedoch viele zusätzliche Probleme. Diese Option wird nicht empfohlen und wird in Zukunft nicht mehr berücksichtigt.
3.3.4. Automatisches Löschen mit Smart Pointer
In diesem Fall gibt die Factory-Funktion keinen Rohzeiger auf eine Schnittstellenklasse zurück, sondern einen entsprechenden Smart-Zeiger. Dieser intelligente Zeiger wird auf der Implementierungsseite erstellt und kapselt das Löschobjekt, das das Implementierungsobjekt automatisch löscht, wenn der intelligente Zeiger (oder seine letzte Kopie) auf der Clientseite den Gültigkeitsbereich verlässt. In diesem Fall ist möglicherweise keine spezielle virtuelle Funktion zum Löschen des Implementierungsobjekts erforderlich, es wird jedoch weiterhin ein geschützter Destruktor benötigt. Es ist erforderlich, die fehlerhafte Verwendung des delete
zu verhindern. (Es ist zu beachten, dass die Wahrscheinlichkeit eines solchen Fehlers merklich verringert ist.) Diese Option wird in Abschnitt 4.3.2 ausführlicher erläutert.
3.4. Andere Optionen zum Verwalten der Lebensdauer einer Instanz einer Implementierungsklasse
In einigen Fällen erhält der Client möglicherweise einen Zeiger auf die Schnittstellenklasse, besitzt diesen jedoch nicht. Die Verwaltung der Lebensdauer des Implementierungsobjekts erfolgt vollständig auf der Implementierungsseite. Ein Objekt kann beispielsweise ein statisches Singleton-Objekt sein (diese Lösung ist typisch für Fabriken). Ein weiteres Beispiel bezieht sich auf die bidirektionale Interaktion, siehe Abschnitt 3.7. Der Client sollte ein solches Objekt nicht löschen, es wird jedoch ein geschützter Destruktor für eine solche Schnittstellenklasse benötigt. Es ist erforderlich, die fehlerhafte Verwendung des delete
zu verhindern.
3.5. Semantik kopieren
Für eine Schnittstellenklasse ist das Erstellen einer Kopie des Implementierungsobjekts mit dem Kopierkonstruktor nicht möglich. Wenn also ein Kopieren erforderlich ist, muss die Klasse über eine virtuelle Funktion verfügen, die eine Kopie des Implementierungsobjekts erstellt und einen Zeiger auf die Schnittstellenklasse zurückgibt. Eine solche Funktion wird oft als virtueller Konstruktor bezeichnet und heißt traditionell Clone()
oder Duplicate()
.
Die Verwendung des Kopierzuweisungsoperators ist nicht verboten, kann jedoch nicht als gute Idee angesehen werden. Der Kopierzuweisungsoperator ist immer gepaart und muss mit dem Kopierkonstruktor gekoppelt werden. Der vom Standard-Compiler generierte Operator ist bedeutungslos und führt nichts aus. Es ist theoretisch möglich, einen Zuweisungsoperator mit anschließender Neudefinition als rein virtuell zu deklarieren. Eine virtuelle Zuweisung wird jedoch nicht empfohlen. Details finden Sie in [Meyers1]. Darüber hinaus sieht die Zuweisung sehr unnatürlich aus: Der Zugriff auf die Objekte der Implementierungsklasse erfolgt normalerweise über einen Zeiger auf die Schnittstellenklasse, sodass die Zuweisung folgendermaßen aussieht:
* = *;
Der Zuweisungsoperator ist am besten verboten, und falls erforderlich, hat eine solche Semantik in der Schnittstellenklasse die entsprechende virtuelle Funktion.
Es gibt zwei Möglichkeiten, die Zuweisung zu verbieten.
- Deklarieren Sie den Zuweisungsoperator als gelöscht (
=delete
). Wenn die Schnittstellenklassen eine Hierarchie bilden, reicht dies in der Basisklasse aus. Der Nachteil dieser Methode ist, dass sie die Implementierungsklasse betrifft, das Verbot gilt auch für sie. - Deklarieren Sie eine geschützte Zuweisungsanweisung mit einer Standarddefinition (
=default
). Dies wirkt sich nicht auf die Implementierungsklasse aus, aber im Fall einer Hierarchie von Schnittstellenklassen muss eine solche Ankündigung in jeder Klasse erfolgen.
3.6. Schnittstellenklassenkonstruktor
Oft wird der Konstruktor einer Schnittstellenklasse nicht deklariert. In diesem Fall generiert der Compiler den Standardkonstruktor, der zum Implementieren der Vererbung erforderlich ist (siehe Abschnitt 1.1). Dieser Konstruktor ist offen, aber ausreichend, um sicher zu sein. Wenn in der Schnittstellenklasse der kopierende Konstruktor als gelöscht deklariert wird ( =delete
), wird die Generierung des Konstruktors durch den Compiler standardmäßig unterdrückt, und ein solcher Konstruktor muss explizit deklariert werden. Es ist natürlich, es mit einer Standarddefinition ( =default
) sicher zu machen. Grundsätzlich kann die Deklaration eines solchen geschützten Konstruktors immer erfolgen. Ein Beispiel finden Sie in Abschnitt 4.4.
3.7. Bidirektionale Interaktion
Schnittstellenklassen sind praktisch für die Verwendung der bidirektionalen Kommunikation. Wenn auf ein Modul über Schnittstellenklassen zugegriffen werden kann, kann der Client auch Implementierungen einiger Schnittstellenklassen erstellen und Zeiger im Modul an diese übergeben. Über diese Zeiger kann das Modul Dienste vom Client empfangen und auch Daten oder Benachrichtigungen an den Client senden.
3.8. Intelligente Zeiger
Da der Zugriff auf Objekte der Implementierungsklasse normalerweise über einen Zeiger erfolgt, ist es selbstverständlich, intelligente Zeiger zu verwenden, um deren Lebensdauer zu steuern. Es ist jedoch zu beachten, dass bei Verwendung der zweiten Option zum Löschen von Objekten mit dem Standard-Smart-Zeiger ein Benutzerlöscher (Typ) oder eine Instanz dieses Typs übertragen werden muss. Wenn dies nicht getan wird, verwendet der Smart Pointer den delete
, um das Objekt zu löschen, und der Code wird einfach nicht kompiliert (dank des geschützten Destruktors). Standard-Smart-Pointer (einschließlich der Verwendung von benutzerdefinierten Entfernern) werden in [Josuttis], [Meyers3] ausführlich erläutert. Ein Beispiel für die Verwendung eines benutzerdefinierten Entferners finden Sie in Abschnitt 4.3.1.
, , , .
3.9. -
- const. , , -, .
3.10. COM-
COM- , , COM — , COM- , C, , . COM- C++ , COM.
3.11.
(API) (SDK). . -, -, . , (Windows DLL), : -. . , , . LoadLibrary()
, -, .
4.
4.1.
, .
class IBase { protected: virtual ~IBase() = default;
.
class IActivatable : public IBase { protected: ~IActivatable() = default;
, , . , IBase
. , (. 1.3). , .
4.2.
class Activator : private IActivatable {
, , , - , .
4.3.
4.3.1.
. - ( IBase
):
struct BaseDeleter { void operator()(IBase* p) const { p->Delete(); } };
std::unique_ptr<>
- :
template <class I> // I — IBase using UniquePtr = std::unique_ptr<I, BaseDeleter>;
, , - , UniquePtr
.
-:
template <class I> // I — - CreateInstance() UniquePtr<I> CreateInstance() { return UniquePtr<I>(I::CreateInstance()); }
:
template <class I> // I — IBase UniquePtr<I> ToPtr(I* p) { return UniquePtr<I>(p); }
std::shared_ptr<>
std::unique_ptr<>
, , std::shared_ptr<>
. Activator
.
auto un1 = CreateInstance<IActivatable>(); un1->Activate(true); auto un2 = ToPtr(IActivatable::CreateInstance()); un2->Activate(true); std::shared_ptr<IActivatable> sh = CreateInstance<IActivatable>(); sh->Activate(true);
( — -):
std::shared_ptr<IActivatable> sh2(IActivatable::CreateInstance());
std::make_shared<>()
, ( ).
: , . : , - . 4.4.
4.3.2.
. -. std::shared_ptr<>
, , ( ). std::shared_ptr<>
( ) - , delete
. std::shared_ptr<>
- ( ) - . .
#include <memory> class IActivatable; using ActPtr = std::shared_ptr<IActivatable>; // class IActivatable { protected: virtual ~IActivatable() = default; // IActivatable& operator=(const IActivatable&) = default; // public: virtual void Activate(bool activate) = 0; static ActPtr CreateInstance(); // - }; // class Activator : public IActivatable { // ... public: Activator(); // ~Activator(); // void Activate(bool activate) override; }; Activator::Activator() {/* ... */} Activator::~Activator() {/* ... */} void Activator::Activate(bool activate) {/* ... */} ActPtr IActivatable::CreateInstance() { return ActPtr(new Activator()); }
- std::make_shared<>()
:
ActPtr IActivatable::CreateInstance() { return std::make_shared<Activator>(); }
std::unique_ptr<>
, , - , .
4.4.
C# Java C++ «», . . IBase
.
class IBase { protected: IBase() = default; virtual ~IBase() = 0;
, Delete()
, .
IBase::~IBase() = default; void IBase::Delete() { delete this; }
IBase
. Delete()
, . - IBase
. Delete()
, - . Delete()
, . , 4.3.1.
5. ,
5.1
, , , , .
, , IException
Exception
.
class IException { friend class Exception; virtual IException* Clone() const = 0; virtual void Delete() = 0; protected: virtual ~IException() = default; public: virtual const char* What() const = 0; virtual int Code() const = 0; IException& operator=(const IException&) = delete; }; class Exception { IException* const m_Ptr; public: Exception(const char* what, int code); Exception(const Exception& src) : m_Ptr(src.m_Ptr->Clone()) {} ~Exception() { m_Ptr->Delete(); } const IException* Ptr() const { return m_Ptr; } };
Exception
, IException
. , throw
, . Exception
, . - , .
Exception
, , .
IException
:
class ExcImpl : IException { friend class Exception; const std::string m_What; const int m_Code; ExcImpl(const char* what, int code); ExcImpl(const ExcImpl&) = default; IException* Clone() const override; void Delete() override; protected: ~ExcImpl() = default; public: const char* What() const override; int Code() const override; }; ExcImpl::ExcImpl(const char* what, int code) : m_What(what), m_Code(code) {} IException* ExcImpl::Clone() const { return new ExcImpl(*this); } void ExcImpl::Delete() { delete this; } const char* ExcImpl::What() const { return m_What.c_str(); } int ExcImpl::Code() const { return m_Code; }
Exception
:
Exception::Exception(const char* what, int code) : m_Ptr(new ExcImpl(what, code)) {}
, — .NET — , — , C++/CLI. , , , C++/CLI.
5.2
- :
template <typename T> class ICollect { protected: virtual ~ICollect() = default; public: virtual ICollect<T>* Clone() const = 0; virtual void Delete() = 0; virtual bool IsEmpty() const = 0; virtual int GetCount() const = 0; virtual T& GetItem(int ind) = 0; virtual const T& GetItem(int ind) const = 0; ICollect<T>& operator=(const ICollect<T>&) = delete; };
, -, .
template <typename T> class ICollect; template <typename T> class Iterator; template <typename T> class Contain { typedef ICollect<T> CollType; CollType* m_Coll; public: typedef T value_type; Contain(CollType* coll); ~Contain();
. , . , , , , - begin()
end()
, . (. [Josuttis]), for
. . , , .
6. -
. -, . . , ++. , .NET, Java Pyton. . , , . .NET Framework C++/CLI C++. .
7.
-, .
.
delete
.- .
- .
.
, delete
. , .
- , . , , delete
.
.
, , , , .
Liste[GoF]
Gamma E., Helm R., Johnson R., Vlissides J. Methoden des objektorientierten Entwurfs. Entwurfsmuster .: Per. aus dem Englischen - St. Petersburg: Peter, 2001.
[Josuttis]
Josattis, Nikolai M. C ++ - Standardbibliothek: Referenzhandbuch, 2. Ausgabe: Per. aus dem Englischen - M.: LLC “I.D. Williams, 2014.
[Dewhurst]
, . C++. .: . . — .: , 2012.
[Meyers1]
, . C++. 35 .: . . — .: , 2000.
[Meyers2]
, . C++. 55 .: . . — .: , 2014.
[Meyers3]
, . C++: 42 C++11 C++14.: . . — .: «.. », 2016.
[Sutter]
, . C++.: . . — : «.. », 2015.
[Sutter/Alexandrescu]
, . , . ++.: . . — .: «.. », 2015.