在 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`);
        }
    }
};`
相关推荐
weixin_4365250729 分钟前
jar包启动使用logs替换nohup日志文件
java·linux·数据库
宠..30 分钟前
创建文本框控件
linux·运维·服务器·开发语言·qt
Bigan(安)34 分钟前
【奶茶Beta专项】【LVGL9.4源码分析】03-显示框架-display
linux·c语言·mcu·arm·unix
mljy.36 分钟前
Linux《进程间关系和守护进程》
linux
win水36 分钟前
十,进程控制
linux·服务器·vim·gcc·g++
摸鱼仙人~1 小时前
VMware虚拟机(以Ubuntu为例)的共享文件夹挂载操作
linux·chrome·ubuntu
ZhongruiRao1 小时前
vscode windows免密登录Linux服务器教程 解决设置后仍需要输入密码的问题
linux·服务器·vscode
Evan芙1 小时前
ifconfig 命令详解
linux·网络·ubuntu
XH-hui1 小时前
【打靶日记】VulNyx 之 Responder
linux·网络安全·vulnyx