大家好!我是大聪明-PLUS!
介绍
C++/CLI 是 .NET Framework 的语言之一,但很少用于开发大型独立项目。它的主要用途是创建程序集,以实现 .NET 与原生(非托管)代码之间的互操作性。因此,C++/CLI 广泛使用句柄类------一种托管类,其成员包含指向原生类的指针。通常,句柄类拥有相应的原生对象,这意味着它必须在适当的时候将其删除。将此类设置为可释放类(即实现 `Dispose` 接口)是很自然的做法System::IDisposable。在 .NET 中实现此接口必须遵循一种称为基本释放 (Basic Dispose) 的特殊模式 [Cwalina]。C++/CLI 的一个显著特点是,编译器几乎负责所有实现此模式的常规工作,而在 C# 中,几乎所有工作都必须手动完成。
1. C++/CLI 中的基本 Dispose 模板
实现这种模式主要有两种方法。
1.1 析构函数和终结器的定义
在这种情况下,托管类必须定义析构函数和终结器,编译器将完成其余工作。
public` `ref` `class` `X`
{
`~X`() {`/* ... */`}
`!X`() {`/* ... */`}
`// ...`
};`
具体来说,编译器会执行以下操作:
- 对于一个类,
X它实现了接口System::IDisposable。 - B
X::Dispose()确保析构函数被调用,基类析构函数被调用(如果有),并且GC::SupressFinalize()。 - 重写
System::Object::Finalize()确保调用终结器和基类终结器(如果有)的地方。
继承关系System::IDisposable可以明确指定,但X::Dispose()不能独立确定。
1.2 使用栈语义
如果一个类包含一个可释放类型的成员,并且该成员使用栈语义声明,则编译器也会实现基本 Dispose 模式。这意味着^声明时使用未暴露的类型名称(' '),并且初始化发生在构造函数的初始化列表中,而不是使用 `init` gcnew。
我们举个例子:
public` `ref` `class` `R` : `System::IDisposable`
{
`public`:
`R`();
`// ...`
};
`public` `ref` `class` `X`
{
`R` `m_R`;
`public`:
`X`()
: `m_R`()
{`/* ... */`}
`// ...`
};`
在这种情况下,编译器会执行以下操作:
- 对于一个类,
X它实现了接口System::IDisposable。 - B
X::Dispose()提供了一个R::Dispose()调用m_R。
最终化取决于相应的类功能R。与之前的情况一样,System::IDisposable可以显式指定继承关系,但X::Dispose()不能独立定义继承关系。当然,该类可能还有其他使用栈语义声明的成员,并且它们的调用也得到保证Dispose()。
2. 托管模板
最后,C++/CLI 的另一个显著特性使得创建描述符类变得极其简单。这些描述符类是托管模板。它们并非泛型,而是真正的模板,就像经典 C++ 中的模板一样,但它们是托管类的模板,而非原生类的模板。实例化这些模板会生成托管类,这些托管类可以用作基类,也可以用作程序集中其他类的成员。
2.1. 智能指针
托管模板允许您创建类似智能指针的类,这些类包含指向原生对象的指针作为成员,并确保在析构函数和终结器中将其删除。在开发自动释放资源的句柄类时,此类智能指针可以用作基类或成员(当然,需要使用栈语义)。
我们举个例子来说明这类模板。第一个模板是基模板,第二个模板用作基类,第三个模板用作类成员。这些模板都有一个用于对象删除的模板参数(原生参数)。默认的删除器类使用运算符删除对象delete。
`
`template` `<typename` `T>`
`struct` `DefDeleter`
{
`void` `operator`()(`T*` `p`) `const` { `delete` `p`; }
};
`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 使用示例
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`(); }
`// ...`
};`
在这些示例中U,类V无需任何额外操作即可释放,只需对指向该类的指针Dispose()调用 `resources` 运算符即可。第二种方法(使用`resources` )允许一个处理程序类管理多个原生类。delete``N``ImplPtrM<>
2.3. 更复杂的最终确定选项
.NET 的终结器机制是一个比较棘手的问题。在正常的应用程序场景中,不应该调用终结器;资源释放会在程序内部完成Dispose()。然而,在紧急情况下,这种情况可能会发生,此时终结器必须能够正常工作。
2.3.1. 阻塞终结器
如果一个本地类包含在一个动态加载和卸载的 DLL 中(使用 `.delete()`)LoadLibrary()/FreeLibrary(),那么可能会出现这样的情况:DLL 卸载后,仍然存在未解析的对象引用该类的实例。在这种情况下,一段时间后,垃圾回收器会尝试终结这些对象,而由于 DLL 已卸载,程序很可能会崩溃。(一个典型的症状是应用程序看似关闭几秒钟后崩溃。)因此,DLL 卸载后,应该禁用终结器。这可以通过对基本 `.delete()` 函数稍作修改来实现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`(); }
`// ...`
};`
加载 DLL 后,需要调用它DllFlag::SetLoaded(true),卸载它之前也需要调用它DllFlag::SetLoaded(false)。
2.3.2. 使用SafeHandle
该类SafeHandle实现了一个相当复杂且高度可靠的终结算法。ImplPtrBase<>可以重构模板以使用该算法SafeHandle。其他模板无需更改。
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`);
}
}
};`