依赖于Windows句柄的类型是否为指针

Is relying on the type of a Windows handle being a pointer ok?

本文关键字:是否 指针 类型 Windows 句柄 依赖于      更新时间:2023-10-16

Windows句柄有时会让人讨厌(使用创建的笔和画笔进行GDI就是一个很好的例子)。RAII解决方案很棒,但是为每个不同类型的句柄制作一个完整的RAII类(Rule of Five)真的很棒吗?当然不是!我能看到的最好的情况是一个完整的通用RAII类,其他类只是定义在应该清理句柄时应该做什么,以及其他特定于句柄的方面。

例如,一个非常简单的模块类可以这样定义(只是一个示例):

struct Module {
    Module() : handle_{nullptr} {}
    Module(HMODULE hm) : handle_{hm, [](HMODULE h){FreeLibrary(h);}} {}
    operator HMODULE() const {return handle_.get();}
private:
    Handle<HMODULE> handle_;
};

这一切都很好,不需要析构函数或任何东西。当然,能够编写不需要析构函数的Handle类也会很好。为什么不使用现有的RAII技术呢?一个想法是使用智能指针指向void,但这行不通。在正常情况下,句柄实际上是这样声明的:

#define DECLARE_HANDLE(n) typedef struct n##__{int i;}*n
DECLARE_HANDLE(HACCEL);
DECLARE_HANDLE(HBITMAP);
DECLARE_HANDLE(HBRUSH);
...

它实际上区分了句柄类型,这很好,但它使使用指向void的智能指针变得不可能。如果句柄根据定义是指针,可以提取类型呢?

我的问题是下面的假设是否安全。它使用一个必须关闭的桌面手柄。除非共享和唯一指针之间的差异(例如,FreeLibrary有自己的引用计数语义),假设句柄是一个指针,并使智能指针指向它所指向的任何东西,或者我不应该使用智能指针并使Handle实现RAII方面本身?

#include <memory>
#include <type_traits>
#include <utility>
#include <windows.h>
int main() {
    using underlying_type = std::common_type<decltype(*std::declval<HDESK>())>::type;
    std::shared_ptr<underlying_type> ptr{nullptr, [](HDESK desk){CloseDesktop(desk);}};
}

我相信所有Windows指针在技术上都是指向系统Windows内核部分内部对象的指针(或者有时,可能是指向内核代码分配的用户端对象,或者是该主题的一些变体)。

我完全不相信你应该把它们当作指针。从纯粹的技术角度来看,它们只是指针。它们不再是"指针",就像C风格的"FILE *"不是指针一样。我不认为你会建议使用shared_ptr<FILE*>来处理关闭文件。

将句柄包装成稍后清理它的东西无论如何都是一个好主意,但我不认为使用智能指针解决方案是正确的解决方案。使用一个知道如何关闭句柄的模板化系统将是理想的。

我想你也需要处理"我想把这个句柄从这里传递到其他地方"在一些好的方式,适用于所有涉及-例如,你有一个函数,以某种方式获取资源,它返回句柄到那些资源-你返回一个已经包装的对象,如果是这样,如何复制工作?

如果在使用另一个句柄之前需要保存一个句柄的副本(例如保存当前的笔,然后设置自定义的笔,然后恢复),该怎么办?

您可以采用的一种方法是使用模板类:

template<typename H, BOOL(WINAPI *Releaser)(H)>
class Handle
{
private:
    H m_handle;
public:
    Handle(H handle) : m_handle(handle) { }
    ~Handle() { (*Releaser)(m_handle); }
};
typedef Handle<HANDLE,&::CloseHandle> RAIIHANDLE;
typedef Handle<HMODULE,&::FreeLibrary> RAIIHMODULE;
typedef Handle<HDESK,&::CloseDesktop> RAIIHDESKTOP;

如果HANDLE不是由BOOL(WINAPI)(HANDLE)类型的函数释放的,那么您可能会遇到问题。如果释放函数只在返回类型上有所不同,您可以将其添加为模板参数,并且仍然使用此解决方案。

从技术上讲,这应该在所有当前版本的Windows下都能很好地工作,而且很难找到一个真正的理由来反对这样做(它实际上是对现有标准库功能的一个非常聪明的使用!),但我仍然不喜欢这个想法,因为:

  1. 句柄是指针,句柄是而不是指针。句柄是不透明的类型,为了兼容性,应该这样对待它们。句柄是指针,句柄不太可能是不同的东西,但这是可能的改变。此外,句柄可能有也可能没有有效的指针值,它们可能有也可能没有32位和64位(比如INVALID_HANDLE_VALUE)下的不同值,或者其他你现在可能无法预见的副作用或行为。假设一个句柄具有某些给定的属性,它可能会工作几十年,但它可能(在理论上)在某些您没有想到的情况下神秘地失败。无可否认,这是不太可能发生的,但它仍然不是100%干净的。
  2. 以这种方式使用智能指针并没有遵循最少惊讶原则。因为,嘿,句柄不是指针。把RAII内置到一个类中,用一个直观的名字命名("Handle","AutoHandle"),不会引起任何人的惊讶。

我相信unique_ptr和shared_ptr都允许您提供自定义删除器。我相信这正是正确管理句柄生命周期所需要的。