【问题标题】:Handles Comparison: empty classes vs. undefined classes vs. void*句柄比较:空类 vs. 未定义类 vs. void*
【发布时间】:2011-05-30 09:03:48
【问题描述】:

Microsoft 的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 方法的一个优点是,他们可以使用空类定义 类型安全 句柄层次结构,这(我认为)其他两个类是不可能的方法,尽管我想知道这种层次结构会给实施带来什么好处?无论如何,还有什么?

编辑:

第二种方法(即使用不完整的类)的一个优点是我们可以防止客户端取消引用句柄(这意味着,这种方法似乎强烈支持封装,我想)。如果尝试取消引用句柄,代码甚至无法编译。还有什么?

第三种方法也有同样的优势,即不能取消引用句柄。

【问题讨论】:

标签: c++ handles incomplete-type empty-class


【解决方案1】:

方法 #1 介于 C 风格和 C++ 接口之间。您必须将句柄作为参数传递,而不是成员函数。暴露多态的优点是可以减少接口中的函数数量,并在编译时检查类型。通常大多数专家更喜欢 pimpl idiom(有时称为编译防火墙)而不是这样的接口。您不能使用方法 #1 与 C 交互,因此最好使用完整的 C++。

方法 #2 是 C 风格的封装和信息隐藏。指针可能(并且通常是)指向真实事物的指针,因此它没有过度设计。库的用户可能不会取消引用该指针。缺点是它不暴露任何多态性。优点是您可以在与用 C 编写的模块交互时使用它。

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

我同意 DeadMG 的观点,即语言中立的面向对象接口在 C++ 中使用起来非常简单和优雅,但是这些也涉及比编译时检查更多的运行时检查,并且当我不需要与其他语言。所以我个人更喜欢方法 #2,如果它需要与 C 或 Pimpl 成语接口,而它只是 C++。

【讨论】:

  • @Öö Tiib :这是一个非常好的帖子。您能否详细说明您使用“多态性”一词的行。特别是方法#1 是如何暴露多态性而方法#2 没有的?
  • @Nawaz : #1 表明 GpTexture 是一个 GpBrush,因此指向 GpTexture 的指针可以用作指向 GpBrush 的指针。 #2 没有公开任何关于前向声明的书。 Book的实现中可能存在多态性,但它是隐藏的,不会被接口暴露出来。
  • @Öö Tiib :感谢您的详细阐述。我也是这么想的。现在,我接受你的回答:-)
【解决方案2】:

方法 3 根本不是很好,因为它允许混合和匹配实际上没有意义的句柄类型,任何接受 HANDLE 的函数都可以接受任何 HANDLE,即使它是编译时可确定的是错误的类型。

方法 1 的缺点是您必须在另一端对它们的实际类型进行大量转换。

方法 2 并没有那么糟糕,只是你不能用它进行任何类型的继承,而不必每次都进行外部查询。

然而,自从编译器发现如何实现高效的虚函数以来,所有这些都完全没有实际意义。 DirectX 和 COM 采用的方法是最好的——它非常灵活、强大且完全类型安全。

它甚至允许一些真正疯狂的事情,比如您可以从 DirectX 接口继承并以这种方式扩展它。这样做的最大优势之一是 Direct2D 和 Direct3D11。它们实际上并不兼容(这确实非常愚蠢),但是您可以定义一个从 ID3D10Device1 继承并转发到 ID3D11Device 的代理类型并解决这样的问题。这种事情永远不会考虑使用上述任何一种方法。

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

【讨论】:

  • 可以使用另一种类型层次结构(带有真正的指针或引用)来代替“一堆强制转换”来实现
  • 我不明白这一点“方法 1 的缺点是你必须在另一端进行一堆转换为它们的实际类型。”。为什么?
  • @Nawaz:因为类被定义为空。当您编写实现代码并包含标头时,您定义的函数将指针指向一堆空类,这些空类显然对实现任何东西都毫无用处,需要强制转换为真正的实现类型。
【解决方案3】:

2 和 3 的类型安全性稍差,因为它们允许使用句柄而不是 void*

无效蓝屏(HBOOK hb){ memset(hb,0,100000); // 没有编译错误 }

【讨论】:

  • 这不是有效的 C++,因为 BOOK* 不能隐式转换为 void*。
  • @DeadMG 在 C 和 C++ 中,任何指针都可以隐式转换为 void*。 C++中只禁止反向转换(从void*到ptr*)
  • @user39662:方法 2 的类型安全性如何降低,而 1 则不是?
猜你喜欢
  • 1970-01-01
  • 2012-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-11
相关资源
最近更新 更多