在 C++/CLI 中开发描述符类

大家好!我是大聪明-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`() {`/* ... */`} 
`// ...`
};`

具体来说,编译器会执行以下操作:

  1. 对于一个类,X它实现了接口System::IDisposable
  2. BX::Dispose()确保析构函数被调用,基类析构函数被调用(如果有),并且GC::SupressFinalize()
  3. 重写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`() 
    {`/* ... */`}
`// ...`
};`

在这种情况下,编译器会执行以下操作:

  1. 对于一个类,X它实现了接口System::IDisposable
  2. BX::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`);
        }
    }
};`
相关推荐
爱凤的小光11 分钟前
Linux清理磁盘技巧---个人笔记
linux·运维
耗同学一米八1 小时前
2026年河北省职业院校技能大赛中职组“网络建设与运维”赛项答案解析 1.系统安装
linux·服务器·centos
charlie1145141911 小时前
现代嵌入式C++教程:C++98——从C向C++的演化(2)
c语言·开发语言·c++·学习·嵌入式·教程·现代c++
知星小度S2 小时前
系统核心解析:深入文件系统底层机制——Ext系列探秘:从磁盘结构到挂载链接的全链路解析
linux
2401_890443022 小时前
Linux 基础IO
linux·c语言
智慧地球(AI·Earth)3 小时前
在Linux上使用Claude Code 并使用本地VS Code SSH远程访问的完整指南
linux·ssh·ai编程
老王熬夜敲代码4 小时前
解决IP不够用的问题
linux·网络·笔记
zly35004 小时前
linux查看正在运行的nginx的当前工作目录(webroot)
linux·运维·nginx
QT 小鲜肉4 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记
问道飞鱼5 小时前
【Linux知识】Linux 虚拟机磁盘扩缩容操作指南(按文件系统分类)
linux·运维·服务器·磁盘扩缩容