En C ++ / CLI, les soi-disant classes de descripteurs sont souvent utilisées - des classes managées qui ont un pointeur sur la classe native en tant que membre. L'article présente un schéma pratique et compact pour gérer la durée de vie de l'objet natif correspondant, basé sur l'utilisation de modèles gérés. Des cas complexes de finalisation sont considérés.
Table des matières
Présentation
1. Modèle d'élimination de base en C ++ / CLI
1.1. Définition du destructeur et du finaliseur
1.2. Utilisation de la sémantique de pile
2. Modèles gérés
2.1. Pointeurs intelligents
2.2. Exemple d'utilisation
2.3. Options de finalisation plus complexes
2.3.1. Verrouillage du finaliseur
2.3.2. Utilisation de SafeHandle
Les références
Présentation
C ++ / CLI - l'un des langages du .NET Framework - est rarement utilisé pour développer de grands projets indépendants. Son objectif principal est de créer des assemblys pour l'interaction .NET avec du code natif (non géré). Par conséquent, les classes appelées classes de descripteurs sont des classes gérées largement utilisées qui ont un pointeur vers la classe native en tant que membre. En règle générale, une telle classe de descripteurs possède l'objet natif correspondant, c'est-à-dire qu'elle doit le supprimer au moment approprié. Il est tout à fait naturel d'exempter une telle classe, c'est-à-dire d'implémenter l' System::IDisposable
. L'implémentation de cette interface dans .NET doit suivre un modèle spécial appelé Basic Dispose [Cwalina]. Une caractéristique remarquable de C ++ / CLI est que le compilateur prend presque tout le travail de routine de mise en œuvre de ce modèle, tandis qu'en C # presque tout doit être fait à la main.
1. Modèle d'élimination de base en C ++ / CLI
Il existe deux méthodes principales pour implémenter ce modèle.
1.1. Définition du destructeur et du finaliseur
Dans ce cas, le destructeur et le finaliseur doivent être définis dans la classe managée, le compilateur fera le reste.
public ref class X { ~X() {}
En particulier, le compilateur effectue les opérations suivantes:
- Pour la classe
X
implémente l' System::IDisposable
. - Dans
X::Dispose()
fournit un appel au destructeur, un appel au destructeur de la classe de base (le cas échéant) et un appel à GC::SupressFinalize()
. - Substitue
System::Object::Finalize()
, où il fournit un appel au finaliseur et aux finaliseurs des classes de base (le cas échéant).
Vous pouvez spécifier l'héritage de System::IDisposable
explicitement, mais vous ne pouvez pas définir X::Dispose()
vous-même.
1.2. Utilisation de la sémantique de pile
Le modèle Basic Dispose est également implémenté par le compilateur si la classe a un membre du type libéré et il est déclaré à l'aide de la sémantique de pile. Cela signifie que le nom du type sans le capuchon (' ^
') est utilisé pour la déclaration et que l'initialisation a lieu dans la liste d'initialisation du constructeur, et non avec gcnew
. La sémantique de la pile est décrite dans [Hogenson].
Voici un exemple:
public ref class R : System::IDisposable { public: R();
Dans ce cas, le compilateur effectue les opérations suivantes:
- Pour la classe
X
implémente l' System::IDisposable
. - Dans
X::Dispose()
fournit un appel à R::Dispose()
pour m_R
.
La finalisation est déterminée par la fonctionnalité de classe R
correspondante. Comme dans le cas précédent, l'héritage de System::IDisposable
peut être spécifié explicitement, mais vous ne pouvez pas définir X::Dispose()
vous-même. Naturellement, la classe peut avoir d'autres membres déclarés en utilisant la sémantique de la pile, et leur appel Dispose()
est également fourni.
2. Modèles gérés
Et enfin, une autre grande fonctionnalité de C ++ / CLI permet de simplifier au maximum la création de classes de descripteurs. Nous parlons de modèles gérés. Ce ne sont pas des génériques, mais de vrais modèles, comme dans le C ++ classique, mais les modèles ne sont pas des classes natives, mais gérées. L'instanciation de ces modèles conduit à la création de classes gérées qui peuvent être utilisées comme classes de base ou comme membres d'autres classes au sein d'un assembly. Les modèles gérés sont décrits dans [Hogenson].
2.1. Pointeurs intelligents
Les modèles gérés vous permettent de créer des classes telles que des pointeurs intelligents qui contiennent un pointeur vers l'objet natif en tant que membre et de fournir sa suppression dans le destructeur et le finaliseur. Ces pointeurs intelligents peuvent être utilisés comme classes de base ou membres (naturellement, en utilisant la sémantique de pile) lors du développement de classes de descripteurs qui sont automatiquement libérées.
Voici un exemple de tels modèles. Le premier modèle est un modèle de base, le second est destiné à être utilisé comme classe de base et le troisième en tant que membre de la classe. Ces modèles ont un paramètre de modèle (natif) conçu pour supprimer un objet. La classe de suppression, par défaut, supprime l'objet avec l'opérateur de delete
.
2.2. Exemple d'utilisation
class N // { public: N(); ~N(); void DoSomething();
Dans ces exemples, les classes U
et V
sont libérées sans effort supplémentaire; leur Dispose()
fournit un appel à l'opérateur de delete
pour un pointeur sur N
La deuxième option, utilisant ImplPtrM<>
, vous permet de gérer plusieurs classes natives dans une seule classe de descripteur.
2.3. Options de finalisation plus complexes
La finalisation est un aspect plutôt problématique de .NET. Dans les scénarios d'application normaux, les finaliseurs ne doivent pas être appelés; les ressources doivent être libérées dans Dispose()
. Mais dans les scénarios d'urgence, cela peut se produire et les finalisateurs devraient fonctionner correctement.
2.3.1. Verrouillage du finaliseur
Si la classe native se trouve dans une DLL qui se charge et se décharge dynamiquement - à l'aide de LoadLibrary()/FreeLibrary()
, une situation peut se produire lorsqu'après le déchargement de la DLL, il existe des objets non libérés qui font référence à des instances de cette classe. Dans ce cas, après un certain temps, le garbage collector tentera de les finaliser, et puisque la DLL est déchargée, le programme se bloquera très probablement. (Une caractéristique est un crash plusieurs secondes après la fermeture apparente de l'application.) Par conséquent, après le déchargement de la DLL, les finaliseurs doivent être bloqués. Cela peut être réalisé avec une petite modification du modèle de base 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 {
Après avoir chargé la DLL, vous devez appeler DllFlag::SetLoaded(true)
et avant de décharger DllFlag::SetLoaded(false)
.
2.3.2. Utilisation de SafeHandle
La classe SafeHandle
implémente un algorithme de finalisation assez complexe et le plus fiable, voir [Richter]. Le ImplPtrBase<>
peut être repensé pour utiliser SafeHandle
. Les modèles restants n'ont pas besoin d'être modifiés.
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); } } };
Les références
[Richter]
Richter, Jeffrey. Programmation sur la plateforme Microsoft .NET Framework 4.5 en C #. 4e éd.: Per. de l'anglais - Saint-Pétersbourg: Peter, 2016.
[Cwalina]
Tsvalina, Krzhishtov. Abrams, Brad. Infrastructure des projets logiciels: conventions, idiomes et modèles pour les bibliothèques .NET réutilisables.: Transl. de l'anglais - M.: LLC «I.D. Williams, 2011.
[Hogenson]
Hogenson, Gordon. C ++ / CLI: langage Visual C ++ pour l'environnement .NET.: Per. de l'anglais - M.: LLC «I.D. Williams, 2007.