我应该为我的不透明对象使用整数ID还是指针?



我正在一些图形API (DirectX9和DirectX11)上面写一个抽象层,我想听听你的意见。

传统上,我会为每个我想抽象的概念创建一个基类。
因此,在典型的OO风格中,我将有一个Shader类和两个子类DX9Shader和DX11Shader。

我会对纹理重复这个过程,等等…当我需要实例化它们时,我有一个抽象工厂,它将根据当前的图形API返回适当的子类。
在RAII之后,返回的指针将封装在std::shared_ptr中。

到目前为止一切顺利,但在我的情况下,这种方法有几个问题:

    我需要一个公共接口来封装这两个api(以及未来的其他api)的功能。
  1. 派生类存储在单独的DLL中(一个用于DX9,一个用于DX11等),并且在客户端中对它们具有shared_ptr是一个诅咒:在退出图形DLL时卸载图形DLL,如果客户端仍然具有shared_ptr到其中一个图形对象boom,由于从卸载的DLL调用代码而崩溃。

这促使我重新设计我做事的方式:我以为我可以只是返回原始指针到资源,并有图形API清洁后,但仍然有悬空指针在客户端和接口问题的问题。我甚至考虑过像COM这样的手动引用计数,但我认为这将是一个倒退(如果我错了请纠正我,来自shared_ptr世界,手动引用计数似乎很原始)。

然后我看到了Humus的作品,他所有的图形类都是由整数id表示的(很像OpenGL所做的)。创建一个新对象只返回它的整数ID,并在内部存储指针;都是完全不透明的!

代表抽象的类(如DX9Shader等)都隐藏在设备API后面,这是唯一的接口。如果想设置纹理,只需调用device->SetTexture(ID),其余的在幕后进行。

缺点是API的隐藏部分是臃肿的,有很多样板代码需要使它工作,我不是一个万能类的粉丝。

有什么想法吗?

你说主要问题是DLL被卸载时仍然有一个指向其内部的指针。嗯…不要那样做。您有一个类实例,其成员是在该DLL中实现的。从根本上说,只要那些类实例存在,DLL被卸载就是一个错误

因此,您需要对如何使用此抽象负责。正如您需要对从DLL加载的任何代码负责一样:来自DLL的东西必须在卸载DLL之前清理干净。你怎么做取决于你自己。您可以设置一个内部引用计数,它对DLL返回的每个对象进行递增,并且只在所有引用对象消失后才卸载DLL。或者任何事情,真的。

毕竟,即使你使用这些不透明的数字或其他什么,如果你在DLL卸载时调用这些数字的API函数之一,会发生什么?哎呀……所以这并不能给你带来任何保护。不管怎样,你都得负责。

你可能没有想到的数字方法的缺点是:

  • 了解对象实际是什么的能力降低。API调用可能会失败,因为您传递了一个不是真正对象的数字。或者更糟的是,如果你将一个着色器对象传递给一个接受纹理的函数,会发生什么?也许我们正在讨论一个函数,它接受一个着色器和一个纹理,而你不小心忘记了参数的顺序?如果这些是对象指针,c++规则甚至不允许编译这些代码。但是对于整数呢?一切都很好;你只会得到运行时错误

  • 性能。每个API调用都必须在哈希表或其他东西中查找这个数字以获得实际的指针来工作。如果它是一个哈希表(即:一个数组),那么它可能相当小。但这仍然是间接的。而且由于您的抽象似乎非常低级,因此在此级别上的任何性能损失都可能在性能关键的情况下造成损害。

  • 缺乏RAII和其他范围界定机制。当然,您可以编写一个类似shared_ptr的对象来创建和删除它们。但是如果你使用一个实际的指针,你就不需要这样做了。

这有关系吗?对于对象的用户来说,它只是一个不透明的句柄。它的实际实现类型并不重要,只要我能把句柄传递给你的API函数,并让它们对这个对象做一些事情。

你可以很容易地改变这些句柄的实现,所以现在让它变得更容易。

只要声明句柄类型为指针或整数的typedef,并确保所有客户端代码都使用typedef名称,那么客户端代码就不依赖于您选择的表示句柄的特定类型。

现在就采用简单的解决方案,如果/当你遇到问题时,因为简单,那就改变它。

关于你的p. 2:客户端总是在库之前卸载。

每个进程都有自己的库依赖树,.exe作为树的根,用户Dll在中间级别,系统库在较低级别。进程从低到高加载,最后加载树根(exe)。从根目录开始卸载进程,最后卸载低级库。这样做是为了防止你所说的情况。

当然,如果你手动加载/卸载库,这个顺序是改变的,你有责任保持指针有效。

最新更新