Desenvolvendo Classes de Descritores C ++ / CLI


Em C ++ / CLI, as chamadas classes de descritor são frequentemente usadas - classes gerenciadas que possuem um ponteiro para a classe nativa como membro. O artigo discute um esquema conveniente e compacto para gerenciar a vida útil do objeto nativo correspondente, com base no uso de modelos gerenciados. Casos complexos de finalização são considerados.




Sumário


1. Introdução
1. Padrão básico de descarte em C ++ / CLI
1.1 Definição de destruidor e finalizador
1.2 Usando semântica de Pilha
2. Modelos gerenciados
2.1 Ponteiros inteligentes
2.2 Exemplo de uso
2.3 Opções de finalização mais complexas
2.3.1 Bloqueio do finalizador
2.3.2 Usando o SafeHandle
Referências



1. Introdução


C ++ / CLI - uma das linguagens do .NET Framework - raramente é usada para desenvolver grandes projetos independentes. Seu principal objetivo é criar assemblies para interação do .NET com código nativo (não gerenciado). Consequentemente, as classes denominadas classes de descritor são amplamente usadas, classes gerenciadas que possuem um ponteiro para a classe nativa como membro. Normalmente, essa classe de descritor possui o objeto nativo correspondente, ou seja, deve excluí-lo no momento apropriado. É bastante natural tornar essa classe isenta, ou seja, implementar a System::IDisposable . A implementação dessa interface no .NET deve seguir um padrão especial chamado Basic Dispose [Cwalina]. Um recurso notável do C ++ / CLI é que o compilador assume quase todo o trabalho de rotina da implementação desse modelo, enquanto no C # quase tudo precisa ser feito manualmente.



1. Padrão básico de descarte em C ++ / CLI


Existem duas maneiras principais de implementar este modelo.



1.1 Definição de destruidor e finalizador


Nesse caso, o destruidor e o finalizador devem ser definidos na classe gerenciada, o compilador fará o resto.


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

Em particular, o compilador faz o seguinte:


  1. Para a classe X implementa a System::IDisposable .
  2. No X::Dispose() fornece uma chamada para o destruidor, uma chamada para o destruidor da classe base (se houver) e uma chamada para GC::SupressFinalize() .
  3. Substitui System::Object::Finalize() , onde fornece uma chamada para o finalizador e finalizadores das classes base (se houver).

Você pode especificar a herança de System::IDisposable explicitamente, mas não pode definir X::Dispose() .



1.2 Usando semântica de Pilha


O padrão Dispose básico também é implementado pelo compilador se a classe tiver um membro do tipo liberado e for declarada usando a semântica da pilha. Isso significa que o nome do tipo sem o limite (' ^ ') é usado para a declaração, e a inicialização ocorre na lista de inicialização do construtor, e não usando gcnew . A semântica da pilha é descrita em [Hogenson].


Aqui está um exemplo:


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

O compilador nesse caso faz o seguinte:


  1. Para a classe X implementa a System::IDisposable .
  2. No X::Dispose() fornece uma chamada para R::Dispose() para m_R .

A finalização é determinada pela funcionalidade correspondente da classe R Como no caso anterior, a herança de System::IDisposable pode ser especificada explicitamente, mas você não pode definir X::Dispose() . Naturalmente, a classe pode ter outros membros declarados usando a semântica da pilha, e sua chamada Dispose() também é fornecida para eles.



2. Modelos gerenciados


E, finalmente, outro ótimo recurso do C ++ / CLI torna possível simplificar a criação de classes de descritores o máximo possível. Estamos falando de modelos gerenciados. Estes não são genéricos, mas modelos reais, como no C ++ clássico, mas os modelos não são nativos, mas classes gerenciadas. A instanciação de tais padrões leva à criação de classes gerenciadas que podem ser usadas como classes base ou como membros de outras classes dentro de um assembly. Modelos gerenciados são descritos em [Hogenson].



2.1 Ponteiros inteligentes


Modelos gerenciados permitem criar classes como ponteiros inteligentes que contêm um ponteiro para o objeto nativo como membro e fornecem sua remoção no destruidor e finalizador. Esses indicadores inteligentes podem ser usados ​​como classes base ou membros (naturalmente, usando semântica de pilha) ao desenvolver classes de descritores que são automaticamente liberadas.


Aqui está um exemplo de tais padrões. O primeiro modelo é um modelo base, o segundo é destinado ao uso como classe base e o terceiro como membro da classe. Esses modelos têm um parâmetro de modelo (nativo) projetado para excluir um objeto. A classe de exclusão, por padrão, exclui o objeto com o 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 Exemplo 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(); } // ... }; 

Nesses exemplos, as classes U e V são liberadas sem nenhum esforço adicional; seu Dispose() fornece uma chamada ao operador de delete para um ponteiro para N A segunda opção, usando ImplPtrM<> , permite gerenciar várias classes nativas em uma única classe de descritor.



2.3 Opções de finalização mais complexas


A finalização é um aspecto bastante problemático do .NET. Em cenários normais de aplicativos, os finalizadores não devem ser chamados; os recursos devem ser liberados em Dispose() . Mas em cenários de emergência, isso pode acontecer e os finalizadores devem funcionar corretamente.



2.3.1 Bloqueio do finalizador


Se a classe nativa estiver localizada em uma DLL que carrega e descarrega dinamicamente - usando LoadLibrary()/FreeLibrary() , pode ocorrer uma situação quando, após o descarregamento da DLL, houver objetos não liberados que tenham referências a instâncias dessa classe. Nesse caso, depois de um tempo, o coletor de lixo tentará finalizá-las e, como a DLL é descarregada, o programa provavelmente falhará. (Um recurso característico é uma falha vários segundos depois que o aplicativo é aparentemente fechado.) Portanto, após o descarregamento da DLL, os finalizadores devem ser bloqueados. Isso pode ser alcançado com uma pequena modificação do modelo básico do 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(); } // ... }; 

Depois de carregar a DLL, você precisa chamar DllFlag::SetLoaded(true) e antes de descarregar DllFlag::SetLoaded(false) .



2.3.2 Usando o SafeHandle


A classe SafeHandle implementa um algoritmo de finalização bastante complexo e mais confiável, consulte [Richter]. O modelo ImplPtrBase<> pode ser reprojetado para usar o SafeHandle . Os demais modelos não precisam ser alterados.


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


Referências


[Richter]
Richter, Jeffrey. Programação na plataforma Microsoft .NET Framework 4.5 em C #. 4a ed.: Per. do inglês - São Petersburgo: Peter, 2016.


[Cwalina]
Tsvalina, Krzhishtov. Abrams, Brad. Infraestrutura de projetos de software: convenções, idiomas e modelos para bibliotecas .NET reutilizáveis.: Transl. do inglês - M .: LLC “I.D. Williams, 2011.


[Hogenson]
Hogenson, Gordon. C ++ / CLI: linguagem Visual C ++ para o ambiente .NET.: Por. do inglês - M .: LLC “I.D. Williams, 2007.



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


All Articles