Desarrollo de clases de descriptor C ++ / CLI


En C ++ / CLI, a menudo se utilizan las llamadas clases descriptivas, clases administradas que tienen un puntero a la clase nativa como miembro. El artículo analiza un esquema conveniente y compacto para administrar la vida útil del objeto nativo correspondiente, basado en el uso de plantillas administradas. Se consideran casos complejos de finalización.




Tabla de contenidos


Introduccion
1. Patrón de eliminación básico en C ++ / CLI
1.1. Definición de destructor y finalizador
1.2. Usando semántica de pila
2. Plantillas gestionadas
2.1. Punteros inteligentes
2.2. Ejemplo de uso
2.3. Opciones de finalización más complejas
2.3.1. Bloqueo finalizador
2.3.2. Usando SafeHandle
Referencias



Introduccion


C ++ / CLI, uno de los lenguajes de .NET Framework, rara vez se usa para desarrollar grandes proyectos independientes. Su objetivo principal es crear ensamblados para la interacción .NET con código nativo (no administrado). En consecuencia, las clases llamadas clases de descriptor son clases administradas ampliamente utilizadas que tienen un puntero a la clase nativa como miembro. Normalmente, dicha clase de descriptor posee el objeto nativo correspondiente, es decir, debe eliminarlo en el momento apropiado. Es bastante natural hacer que tal clase esté exenta, es decir, implementar la System::IDisposable . La implementación de esta interfaz en .NET debe seguir un patrón especial llamado Disposición básica [Cwalina]. Una característica notable de C ++ / CLI es que el compilador realiza casi todo el trabajo rutinario de implementación de esta plantilla, mientras que en C # casi todo tiene que hacerse a mano.



1. Patrón de eliminación básico en C ++ / CLI


Hay dos formas principales de implementar esta plantilla.



1.1. Definición de destructor y finalizador


En este caso, el destructor y el finalizador deben definirse en la clase administrada, el compilador hará el resto.


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

En particular, el compilador hace lo siguiente:


  1. Para la clase X implementa la System::IDisposable .
  2. En X::Dispose() proporciona una llamada al destructor, una llamada al destructor de la clase base (si la hay) y una llamada a GC::SupressFinalize() .
  3. Invalida System::Object::Finalize() , donde proporciona una llamada al finalizador y finalizadores de las clases base (si las hay).

Puede especificar la herencia de System::IDisposable explícitamente, pero no puede definir X::Dispose() usted mismo.



1.2. Usando semántica de pila


El compilador también implementa el patrón de Disposición básica si la clase tiene un miembro del tipo liberado y se declara utilizando semántica de pila. Esto significa que la declaración usa un nombre de tipo sin un límite (' ^ '), y la inicialización ocurre en la lista de inicialización del constructor, y no usa gcnew . La semántica de la pila se describe en [Hogenson].


Aquí hay un ejemplo:


 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(/*  */)    {/* ... */} // ... }; 

El compilador en este caso hace lo siguiente:


  1. Para la clase X implementa la System::IDisposable .
  2. En X::Dispose() proporciona una llamada a R::Dispose() para m_R .

La finalización está determinada por la correspondiente funcionalidad de clase R Como en el caso anterior, la herencia de System::IDisposable se puede especificar explícitamente, pero no puede definir X::Dispose() usted mismo. Naturalmente, la clase puede tener otros miembros declarados utilizando la semántica de la pila, y su llamada Dispose() también se proporciona para ellos.



2. Plantillas gestionadas


Y finalmente, otra gran característica de C ++ / CLI hace que sea posible simplificar la creación de clases de descriptores tanto como sea posible. Estamos hablando de plantillas administradas. Estos no son genéricos, sino plantillas reales, como en C ++ clásico, pero las plantillas no son nativas, sino clases administradas. La creación de instancias de tales patrones conduce a la creación de clases administradas que pueden usarse como clases base o como miembros de otras clases dentro de un ensamblado. Las plantillas administradas se describen en [Hogenson].



2.1. Punteros inteligentes


Las plantillas administradas le permiten crear clases como punteros inteligentes que contienen un puntero al objeto nativo como miembro y proporcionan su eliminación en el destructor y el finalizador. Tales punteros inteligentes se pueden usar como clases base o miembros (naturalmente, usando semántica de pila) cuando se desarrollan clases de descriptores que se liberan automáticamente.


Aquí hay un ejemplo de tales patrones. La primera plantilla es una plantilla base, la segunda está diseñada para usarse como una clase base y la tercera como miembro de la clase. Estas plantillas tienen un parámetro de plantilla (nativo) diseñado para eliminar un objeto. La clase de eliminación, de forma predeterminada, elimina el objeto con el operador de 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. Ejemplo de uso


 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(); } // ... }; 

En estos ejemplos, las clases U y V se liberan sin ningún esfuerzo adicional; su Dispose() proporciona una llamada al operador de delete para un puntero a N La segunda opción, usando ImplPtrM<> , le permite administrar múltiples clases nativas en una sola clase de descriptor.



2.3. Opciones de finalización más complejas


La finalización es un aspecto bastante problemático de .NET. En escenarios de aplicación normales, no se debe llamar a los finalizadores; los recursos se deben liberar en Dispose() . Pero en situaciones de emergencia esto puede suceder y los finalizadores deberían funcionar correctamente.



2.3.1. Bloqueo finalizador


Si la clase nativa se encuentra en una DLL que se carga y descarga dinámicamente, usando LoadLibrary()/FreeLibrary() , entonces puede surgir una situación cuando después de descargar la DLL hay objetos inéditos que tienen referencias a instancias de esta clase. En este caso, después de un tiempo, el recolector de basura intentará finalizarlos y, dado que la DLL está descargada, lo más probable es que el programa se bloquee. (Una característica característica es un bloqueo varios segundos después de que la aplicación aparentemente se cierra). Por lo tanto, después de descargar la DLL, los finalizadores deben estar bloqueados. Esto se puede lograr con una pequeña modificación de la plantilla básica ImplPtrBase .


 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(); } // ... }; 

Después de cargar la DLL, debe llamar a DllFlag::SetLoaded(true) y antes de descargar DllFlag::SetLoaded(false) .



2.3.2. Usando SafeHandle


La clase SafeHandle implementa un algoritmo de finalización bastante complejo y confiable, ver [Richter]. La plantilla ImplPtrBase<> se puede rediseñar para usar SafeHandle . Las plantillas restantes no necesitan ser cambiadas.


 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);        }    } }; 


Referencias


[Richter]
Richter, Jeffrey. Programación en la plataforma Microsoft .NET Framework 4.5 en C #. 4ª ed .: Per. del ingles - San Petersburgo: Peter, 2016.


[Cwalina]
Tsvalina, Krzhishtov. Abrams, Brad. Infraestructura de proyectos de software: convenciones, modismos y plantillas para bibliotecas .NET reutilizables.: Transl. del ingles - M .: LLC "I.D. Williams, 2011.


[Hogenson]
Hogenson, Gordon. C ++ / CLI: lenguaje Visual C ++ para el entorno .NET.: Por. del ingles - M .: LLC "I.D. Williams, 2007.



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


All Articles