Entwickeln von C ++ / CLI-Deskriptorklassen


In C ++ / CLI werden häufig die sogenannten Deskriptorklassen verwendet - verwaltete Klassen, die einen Zeiger auf die native Klasse als Mitglied haben. Der Artikel beschreibt ein praktisches und kompaktes Schema zum Verwalten der Lebensdauer des entsprechenden nativen Objekts basierend auf der Verwendung verwalteter Vorlagen. Komplexe Fälle der Finalisierung werden berücksichtigt.




Inhaltsverzeichnis


Einführung
1. Grundlegendes Entsorgungsmuster in C ++ / CLI
1.1. Definition von Destruktor und Finalizer
1.2. Stapelsemantik verwenden
2. Verwaltete Vorlagen
2.1. Intelligente Zeiger
2.2. Anwendungsbeispiel
2.3. Komplexere Finalisierungsoptionen
2.3.1. Finalizer Lock
2.3.2. Verwenden von SafeHandle
Referenzliste



Einführung


C ++ / CLI - eine der Sprachen von .NET Framework - wird selten zum Entwickeln großer unabhängiger Projekte verwendet. Der Hauptzweck besteht darin, Assemblys für die .NET-Interaktion mit nativem (nicht verwaltetem) Code zu erstellen. Dementsprechend sind Klassen, die als Deskriptorklassen bezeichnet werden, weit verbreitete verwaltete Klassen, die einen Zeiger auf die native Klasse als Mitglied haben. In der Regel besitzt eine solche Deskriptorklasse das entsprechende native Objekt, dh sie muss es zum geeigneten Zeitpunkt löschen. Es ist ganz natürlich, eine solche Klasse System::IDisposable die System::IDisposable . Die Implementierung dieser Schnittstelle in .NET muss einem speziellen Muster namens Basic Dispose [Cwalina] folgen. Ein bemerkenswertes Merkmal von C ++ / CLI ist, dass der Compiler fast die gesamte Routinearbeit zur Implementierung dieser Vorlage übernimmt, während in C # fast alles von Hand erledigt werden muss.



1. Grundlegendes Entsorgungsmuster in C ++ / CLI


Es gibt zwei Möglichkeiten, diese Vorlage zu implementieren.



1.1. Definition von Destruktor und Finalizer


In diesem Fall müssen der Destruktor und der Finalizer in der verwalteten Klasse definiert werden, der Compiler erledigt den Rest.


 public ref class X {    ~X() {/* ... */} //     !X() {/* ... */} //  // ... }; 

Insbesondere macht der Compiler Folgendes:


  1. Implementiert für Klasse X die System::IDisposable .
  2. In X::Dispose() ein Aufruf an den Destruktor, ein Aufruf an den Destruktor der Basisklasse (falls vorhanden) und ein Aufruf an GC::SupressFinalize() .
  3. Überschreibt System::Object::Finalize() , wobei der Finalizer und die Finalizer der Basisklassen (falls vorhanden) aufgerufen werden.

Sie können die Vererbung von System::IDisposable explizit angeben, X::Dispose() selbst definieren.



1.2. Stapelsemantik verwenden


Das Basic Dispose-Muster wird auch vom Compiler implementiert, wenn die Klasse ein Mitglied des freigegebenen Typs hat und mithilfe der Stapelsemantik deklariert wird. Dies bedeutet, dass der Typname ohne die Kappe (' ^ ') für die Deklaration verwendet wird und die Initialisierung in der Konstruktorinitialisierungsliste erfolgt und nicht gcnew . Die Semantik des Stapels ist in [Hogenson] beschrieben.


Hier ist ein Beispiel:


 public ref class R : System::IDisposable { public:    R(/*  */); //  // ... }; public ref class X {    R m_R; //   R^ m_R public:    X(/*  */) //         : m_R(/*  */) //   m_R = gcnew R(/*  */)    {/* ... */} // ... }; 

Der Compiler führt in diesem Fall Folgendes aus:


  1. Implementiert für Klasse X die System::IDisposable .
  2. m_R in X::Dispose() einen Aufruf von R::Dispose() für m_R .

Die Finalisierung wird durch die entsprechende Klasse- R Funktionalität bestimmt. Wie im vorherigen Fall kann die Vererbung von System::IDisposable explizit angegeben werden, Sie können X::Dispose() selbst definieren. Natürlich kann die Klasse andere Mitglieder haben, die unter Verwendung der Semantik des Stapels deklariert wurden, und ihr Dispose() -Aufruf wird auch für sie bereitgestellt.



2. Verwaltete Vorlagen


Und schließlich ermöglicht eine weitere großartige Funktion von C ++ / CLI die Erstellung von Deskriptorklassen so weit wie möglich. Wir sprechen über verwaltete Vorlagen. Dies sind keine Generika, sondern echte Vorlagen, wie in klassischem C ++, aber Vorlagen sind keine nativen, sondern verwaltete Klassen. Die Instanziierung solcher Muster führt zur Erstellung verwalteter Klassen, die als Basisklassen oder als Mitglieder anderer Klassen innerhalb einer Assembly verwendet werden können. Verwaltete Vorlagen sind in [Hogenson] beschrieben.



2.1. Intelligente Zeiger


Mit verwalteten Vorlagen können Sie Klassen wie intelligente Zeiger erstellen, die einen Zeiger auf das native Objekt als Mitglied enthalten, und dessen Entfernung im Destruktor und Finalizer bereitstellen. Solche intelligenten Zeiger können als Basisklassen oder Mitglieder (natürlich unter Verwendung der Stapelsemantik) verwendet werden, wenn Deskriptorklassen entwickelt werden, die automatisch freigegeben werden.


Hier ist ein Beispiel für solche Muster. Die erste Vorlage ist eine Basisvorlage, die zweite ist als Basisklasse und die dritte als Mitglied der Klasse vorgesehen. Diese Vorlagen verfügen über einen Vorlagenparameter (nativ), mit dem ein Objekt gelöscht werden kann. Die Löschklasse löscht das Objekt standardmäßig mit dem delete .


 //  , -  , T —   template <typename T> struct DefDeleter {    void operator()(T* p) const { delete p; } }; //  , //      //  , T —  , D — - template <typename T, typename D> public ref class ImplPtrBase : System::IDisposable {    T* m_Ptr;    void Delete()    {        if (m_Ptr != nullptr)        {            D del;            del(m_Ptr);            m_Ptr = nullptr;        }    }    ~ImplPtrBase() { Delete(); }    !ImplPtrBase() { Delete(); } protected:    ImplPtrBase(T* p) : m_Ptr(p) {}    T* Ptr() { return m_Ptr; } }; //        template <typename T, typename D = DefDeleter<T>> public ref class ImplPtr : ImplPtrBase<T, D> { protected:    ImplPtr(T* p) : ImplPtrBase(p) {} public:    property bool IsValid    {        bool get() { return (ImplPtrBase::Ptr() != nullptr); }    } }; //        template <typename T, typename D = DefDeleter<T>> public ref class ImplPtrM sealed : ImplPtrBase<T, D> { public:    ImplPtrM(T* p) : ImplPtrBase(p) {}    operator bool() { return ( ImplPtrBase::Ptr() != nullptr); }    T* operator->() { return ImplPtrBase::Ptr(); }    T* Get() { return ImplPtrBase::Ptr(); } }; 


2.2. Anwendungsbeispiel


 class N //   { public:    N();    ~N();    void DoSomething(); // ... }; using NPtr = ImplPtr<N>; //   public ref class U : NPtr //  - { public:    U() : NPtr(new N()) {}    void DoSomething() { if (IsValid) Ptr()->DoSomething(); } // ... }; public ref class V //  -,   {    ImplPtrM<N> m_NPtr; //   public:    V() : m_NPtr(new N()) {}    void DoSomething() { if (m_NPtr) m_NPtr->DoSomething(); } // ... }; 

In diesen Beispielen werden die Klassen U und V ohne zusätzlichen Aufwand freigegeben, und Dispose() ruft den delete für einen Zeiger auf N Mit der zweiten Option, die ImplPtrM<> , können Sie mehrere native Klassen in einer einzigen Deskriptorklasse verwalten.



2.3. Komplexere Finalisierungsoptionen


Die Finalisierung ist ein ziemlich problematischer Aspekt von .NET. In normalen Anwendungsszenarien sollten Finalizer nicht aufgerufen werden, Ressourcen sollten in Dispose() freigegeben werden. In Notfallszenarien kann dies jedoch passieren und Finalizer sollten ordnungsgemäß funktionieren.



2.3.1. Finalizer Lock


Befindet sich die native Klasse in einer DLL, die mithilfe von LoadLibrary()/FreeLibrary() dynamisch LoadLibrary()/FreeLibrary() , kann es vorkommen, dass nach dem Entladen der DLL unveröffentlichte Objekte vorhanden sind, die Verweise auf Instanzen dieser Klasse enthalten. In diesem Fall versucht der Garbage Collector nach einer Weile, sie zu finalisieren, und da die DLL entladen ist, stürzt das Programm höchstwahrscheinlich ab. (Ein charakteristisches Merkmal ist ein Absturz einige Sekunden, nachdem die Anwendung anscheinend geschlossen wurde.) Daher müssen die Finalizer nach dem Entladen der DLL blockiert werden. Dies kann mit einer kleinen Änderung der grundlegenden ImplPtrBase Vorlage erreicht werden.


 public ref class DllFlag { protected:    static bool s_Loaded = false; public:    static void SetLoaded(bool loaded) { s_Loaded = loaded; } }; template <typename T, typename D> public ref class ImplPtrBase : DllFlag, System::IDisposable { // ...    !ImplPtrBase() { if (s_Loaded) Delete(); } // ... }; 

Nach dem Laden der DLL müssen Sie DllFlag::SetLoaded(true) und vor dem Entladen von DllFlag::SetLoaded(false) .



2.3.2. Verwenden von SafeHandle


Die SafeHandle Klasse implementiert einen ziemlich komplexen und zuverlässigsten Finalisierungsalgorithmus, siehe [Richter]. Die ImplPtrBase<> kann für die Verwendung von SafeHandle neu SafeHandle . Die restlichen Vorlagen müssen nicht geändert werden.


 using SH = System::Runtime::InteropServices::SafeHandle; using PtrType = System::IntPtr; template <typename T, typename D> public ref class ImplPtrBase : SH { protected:    ImplPtrBase(T* p) : SH(PtrType::Zero, true)    {        handle = PtrType(p);    }    T* Ptr() { return static_cast<T*>(handle.ToPointer()); }    bool ReleaseHandle() override    {        if (!IsInvalid)        {            D del;            del(Ptr());            handle = PtrType::Zero;        }        return true;    } public:    property bool IsInvalid    {        bool get() override        {            return (handle == PtrType::Zero);        }    } }; 


Referenzliste


[Richter]
Richter, Jeffrey. Programmieren auf der Plattform Microsoft .NET Framework 4.5 in C #. 4. Aufl.: Per. aus dem Englischen - St. Petersburg: Peter, 2016.


[Cwalina]
Tsvalina, Krzhishtov. Abrams, Brad. Infrastruktur von Softwareprojekten: Konventionen, Redewendungen und Vorlagen für wiederverwendbare .NET-Bibliotheken .: Transl. aus dem Englischen - M.: LLC “I.D. Williams, 2011.


[Hogenson]
Hogenson, Gordon. C ++ / CLI: Visuelle C ++ - Sprache für die .NET-Umgebung .: Per. aus dem Englischen - M.: LLC “I.D. Williams, 2007.



Source: https://habr.com/ru/post/de426411/


All Articles