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() {}
Em particular, o compilador faz o seguinte:
- Para a classe
X
implementa a System::IDisposable
. - 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()
. - 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();
O compilador nesse caso faz o seguinte:
- Para a classe
X
implementa a System::IDisposable
. - 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
.
2.2 Exemplo de uso
class N // { public: N(); ~N(); void 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 {
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.