In C ++ / CLI, so-called descriptor classes are often used — managed classes that have a pointer to their native class as a member. The article discusses a convenient and compact scheme for controlling the lifetime of the corresponding native object, based on the use of controlled patterns. The complex cases of finalization are considered.
Introduction
1. Basic Dispose pattern in C ++ / CLI
1.1. Definition of destructor and finalizer
1.2. Using stack semantics
2. Managed templates
2.1. Smart Pointers
2.2. Usage example
2.3. More complex options for finalizing
2.3.1. Blocking finalizers
2.3.2. Use SafeHandle
Bibliography
C ++ / CLI, one of the languages of the .NET Framework, is rarely used to develop large, stand-alone projects. Its main purpose is to create assemblies for .NET interaction with native (unmanaged) code. Accordingly, classes called descriptor classes, managed classes that have a pointer to the native class as a member, are widely used. Typically, such a descriptor class owns the corresponding native object, that is, it must delete it at the appropriate time. It is quite natural to make such a class free, that is, implementing the System::IDisposable
. The implementation of this interface in .NET must follow a special pattern called Basic Dispose [Cwalina]. A remarkable feature of C ++ / CLI is that the compiler takes on almost all the routine work of implementing this template, whereas in C # almost everything has to be done by hand.
There are two main ways to implement this template.
In this case, the destructor and finalizer must be defined in the managed class, the compiler will do the rest.
public ref class X { ~X() {/* ... */} // !X() {/* ... */} // // ... };
In particular, the compiler does the following:
X
implements the System::IDisposable
.X::Dispose()
it calls the destructor, calls the base class destructor (if any) and calls GC::SupressFinalize()
.System::Object::Finalize()
, where it provides a call to the finalizer and finalizers of base classes (if any).Inheritance from System::IDisposable
can be specified explicitly, but you cannot define X::Dispose()
yourself.
The Basic Dispose pattern is also implemented by the compiler if the class has a member of the type being released and it is declared using stack semantics. This means that the name of the type without a cap (' ^
') is used for the declaration, and initialization occurs in the constructor initialization list, and not with gcnew
. Stack semantics is described in [Hogenson].
Let's give an example:
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(/* */) {/* ... */} // ... };
The compiler in this case does the following:
X
implements the System::IDisposable
.X::Dispose()
provides an R::Dispose()
m_R
for m_R
.Finalization is determined by the corresponding class R
functionality. As in the previous case, the inheritance from System::IDisposable
can be specified explicitly, and you cannot define X::Dispose()
yourself. Naturally, the class may have other members declared using stack semantics, and Dispose()
also provided for them.
And finally, another great feature of C ++ / CLI makes it as easy as possible to create descriptor classes. We are talking about managed templates. These are not generics, but real patterns, as in classic C ++, but patterns of not native, but managed classes. Instantiating such templates results in the creation of managed classes that can be used as base classes or members of other classes within an assembly. Managed templates are described in [Hogenson].
Managed templates allow you to create classes of type of smart pointers, which contain a pointer to the native object as a member and ensure its removal in the destructor and finalizer. Such smart pointers can be used as base classes or members (of course, using the stack semantics) when developing descriptor classes, which are automatically released.
Let us give an example of such patterns. The first is the base template, the second is intended for use as a base class and the third as a member of the class. These templates have a template parameter (native), designed to delete an object. The default delete class deletes an object with the delete
operator.
// , - , 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(); } };
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 these examples, the classes U
and V
become free without any additional effort; their Dispose()
provides a call to the delete
operator for the pointer to N
The second option, using ImplPtrM<>
, allows you to manage several native classes in the same descriptor class.
Finalization is quite a problematic aspect of the work. NET. In normal application scenarios, finalizers should not be called; resource release occurs in Dispose()
. But in emergency scenarios, this can happen and finalizers should work correctly.
If the native class is in a DLL that is loaded and unloaded dynamically - using LoadLibrary()/FreeLibrary()
, then there may be a situation when, after unloading the DLL, there are unresolved objects that have references to instances of this class. In this case, after some time the garbage collector will try to finalize them, and since the DLL is unloaded, the program is likely to crash. (A characteristic sign is crash a few seconds after the application closes visually.) Therefore, after unloading the DLL, finalizers should be blocked. This can be achieved by a small modification of the base ImplPtrBase
template.
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(); } // ... };
After loading the DLL, you need to call DllFlag::SetLoaded(true)
, and before unloading DllFlag::SetLoaded(false)
.
SafeHandle
The SafeHandle
class implements a rather complicated and most reliable algorithm for finalization, see [Richter]. The ImplPtrBase<>
template can be redesigned so that it uses SafeHandle
. The rest of the templates do not need to be changed.
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); } } };
[Richter]
Richter, Jeffrey. Programming on the Microsoft .NET Framework 4.5 in C #. 4th ed .: Trans. from English - SPb .: Peter, 2016.
[Cwalina]
Tsvalina, Křishtov. Abrams, Brad. The infrastructure of software projects: agreements, idioms and templates for reusable .NET libraries .: Trans. from English - M .: OOO “I.D. Williams, 2011.
[Hogenson]
Hohenson, Gordon. C ++ / CLI: Visual C ++ language for the .NET environment.: Per. from English - M .: OOO “I.D. Williams, 2007.
Source: https://habr.com/ru/post/426411/
All Articles