处理句柄比较:空类 vs 未定义类 vs void*

11

微软的GDI+定义了许多空类用于内部处理。例如,(源自GdiPlusGpStubs.h

//Approach 1

class GpGraphics {};

class GpBrush {};
class GpTexture : public GpBrush {};
class GpSolidFill : public GpBrush {};
class GpLineGradient : public GpBrush {};
class GpPathGradient : public GpBrush {};
class GpHatch : public GpBrush {};

class GpPen {};
class GpCustomLineCap {};

还有两种定义句柄的方法。它们分别是:

//Approach 2
class BOOK;  //no need to define it!
typedef BOOK *PBOOK;
typedef PBOOK HBOOK; //handle to be used internally

//Approach 3
typedef void* PVOID;
typedef PVOID HBOOK; //handle to be used internally
我想知道每种方法的优缺点。
Microsoft的方法之一的优点是,他们可以使用空类定义类型安全handles层次结构,这(我认为)其他两种方法不可能做到,尽管我想知道这个层次结构会给实现带来什么好处?还有呢?
编辑:
第二种方法(即使用不完整的类)的一个优点是,我们可以防止客户端对句柄进行解除引用(也就是说,这种方法似乎强烈支持封装性)。如果试图对句柄进行解除引用,代码甚至都不会编译。还有呢?
第三种方法也具有相同的优点,您无法对句柄进行解除引用。

1
相关(不完全相同)问题在这里:https://dev59.com/9nRA5IYBdhLWcg3wwwvD。不幸的是,从中无法获得太多信息。 - Jon
我删除了C标签,因为问题不能与选项#1相关联。 - Puppy
3个回答

3

方法一介于C风格和C++接口之间。不同于成员函数,你需要将句柄作为参数传递。暴露多态的优点是可以减少接口中的函数数量,并在编译时检查类型。通常大部分专家更喜欢使用pimpl惯用语法(有时称为编译防火墙)而不是这种接口。你不能使用方法一与C进行接口,因此最好完全使用C++。

方法二是C风格的封装和信息隐藏。指针可能是(并且经常是)指向真实事物的指针,因此它没有过度设计。库的用户可能不会解引用该指针。缺点是它不公开任何多态性。优点是当与使用C编写的模块进行接口时可以使用它。

方法三是过度抽象的C风格封装。指针可能根本不是指针,因为库的用户不应该转换、释放或解引用它。优点是它可以携带异常或错误值,缺点是大部分内容必须在运行时检查。

我同意DeadMG的观点,即与语言无关的面向对象接口非常易于使用和优雅,但这些接口涉及更多的运行时检查而不是编译时检查,在不需要与其他语言进行接口时过于冗余。因此,如果需要与C进行接口,则我个人更喜欢方法二;如果只涉及C++,则更喜欢Pimpl惯用语法。


@Öö Tiib:这篇文章很好,你能否详细解释一下你使用“多态性”这个词组的那几行代码呢?特别是为什么方法一展现了多态性而方法二却没有? - Nawaz
@Nawaz:#1表明GpTexture是GpBrush,因此可以将指向GpTexture的指针用作指向GpBrush的指针。#2对于前向声明的Book没有任何暴露。Book的实现中可能存在多态性,但它被隐藏起来,不会通过接口暴露出来。 - Öö Tiib
@Öö Tiib:感谢您的详细解释。我也是这么想的。现在,我接受了您的答案 :-) - Nawaz

2

方法3并不好,因为它允许混用并匹配实际上不合理的句柄类型,任何需要句柄的函数都可以使用任何句柄类型,即使在编译时确定那是错误的类型。

方法1的缺点是你必须在另一端进行大量强制类型转换。

方法2还不错,但如果没有外部查询,就不能进行任何继承。

然而,自从编译器发现了如何实现高效的虚函数以来,所有这些都已经无关紧要了。DirectX和COM采用的方法是最好的-它非常灵活、强大且完全类型安全。

它甚至允许一些真正疯狂的事情,比如你可以从DirectX接口继承并以这种方式扩展它。其中一个最大的优点是Direct2D和Direct3D11。它们实际上不兼容(这真的非常愚蠢),但你可以定义一个代理类型,它继承自ID3D10Device1并转发到ID3D11Device,并通过这种方式解决问题。使用以上任何一种方法都无法想象出这种类型的操作。

哦,最后一件事:你真的不应该将你的类型命名为全大写。


可以使用另一种类型层次结构(具有实际指针或引用)来代替“大量转换”,进行实现。 - user396672
我不理解这句话的意思:“方法1的缺点是在另一端必须进行大量转换以获取它们的实际类型。”为什么? - Nawaz
@Nawaz:因为这些类被定义为空。当你编写实现代码并包含头文件时,你正在定义函数,这些函数接受指向一堆空类的指针,这些类显然对于实现任何东西都是无用的,需要将其转换为真正的实现类型。 - Puppy

1

2和3略微不太类型安全,因为它们允许使用句柄而不是void*

void bluescreeen(HBOOK hb){
  memset(hb,0,100000); // 没有编译错误
}

这不是有效的C++代码,因为BOOK不能隐式转换为void - Puppy
@DeadMG 在C和C++中,任何指针都可以隐式转换为void。但是在C++中禁止从void到ptr*的反向转换。 - user396672
@user39662: 为什么方法2不够类型安全而方法1是呢? - Nawaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接