【问题标题】:Should I use integer ID or pointers for my opaque objects?我应该为我的不透明对象使用整数 ID 还是指针?
【发布时间】:2012-01-09 13:44:15
【问题描述】:

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

传统上,我会为每个要抽象的概念创建一个基类。
因此,在典型的 OO 方式中,我会有一个类 Shader 和 2 个子类 DX9Shader 和 DX11Shader。

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

到目前为止一切顺利,但就我而言,这种方法存在一些问题:

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

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

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

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

缺点是 API 的隐藏部分过于臃肿,需要大量样板代码才能使其工作,而且我不喜欢全能类。

有什么想法/想法吗?

【问题讨论】:

    标签: c++ oop directx abstraction leaky-abstraction


    【解决方案1】:

    这有关系吗?对于对象的用户来说,它只是一个不透明的句柄。它的实际实现类型无关紧要,只要我可以将句柄传递给您的 API 函数并让它们对对象进行处理。

    您可以轻松地更改这些句柄的实现,所以现在让您更容易做到。

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

    现在就选择简单的解决方案,如果/当您遇到问题时,因为它简单,请更改它。

    【讨论】:

    • 这就是问题所在,两者都不简单:返回一个原始指针 我需要为每个类提供一个公共接口并处理无效指针。返回一个 id 我需要将所有实现代码移动到一个类中并实现一些样板代码。对我来说,这似乎是一个严格的权衡;目前我已经实现并且无法决定使用哪一个。我同意对于客户来说这并不重要,但这都是关于这里的实现细节。我也同意我应该选择更简单的版本,如果有的话!
    • 您不需要将所有内容都放在一个类中以使整数 id 起作用。如果使用指针,您不需要为每个类提供公共接口。 (就像我说的,客户端可以将指针视为一个不透明的句柄,就像整数 id 一样传递,然后你在内部将它当作指针使用)
    • 这是一个很好的观点,我以前在 C 中看到过这种范式。句柄将是 typedef void* 并且客户端使用一组函数将该句柄作为参数。我只是认为使用 C++ 我们会有更好的工具/方法来处理这些情况。
    • 嗯,你有更多工具来处理这些情况。哪个更好取决于您的要求。 :)
    【解决方案2】:

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

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

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

    【讨论】:

    • 是的,这正是我的情况,我手动加载图形 dll,因为在某些系统上它们可能无法加载(例如 Windows XP 上的 DX11)。
    【解决方案3】:

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

    因此,您需要对如何使用此抽象负责。正如您需要对从 DLL 加载的任何代码负责一样:必须在卸载 DLL 之前清理来自 DLL 的内容。你如何做到这一点取决于你。您可以有一个内部引用计数,该计数会随着 DLL 返回的每个对象而递增,并且仅在所有引用的对象消失后才卸载 DLL。或者任何东西,真的。

    毕竟,即使您使用这些不透明的数字或其他什么,如果您在卸载 DLL 时对该数字调用其中一个 API 函数会发生什么?哎呀...所以它并没有真正为您购买任何保护。无论哪种方式,你都必须负责。

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

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

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

    • 缺乏 RAII 和其他范围机制。当然,您可以编写一个 shared_ptr-esque 对象来创建和删除它们。但是,如果您使用的是实际指针,则不必这样做。

    这似乎不值得。

    【讨论】:

    • 我同意你的观点,但我不能对客户对我返回给他们的指针所做的事情负责。在我这边,我进行清理工作,但如果他们持有指针的时间超过了他们应该做的事情,我该怎么办?这个想法是使用 shared_ptr 为我做引用计数,所以我应该在卸载库之前检查所有对象的引用计数吗?如果客户忘记释放他/她的 shared_ptr 会发生什么?然后我们遇到了泄漏/死锁。
    • 我认为检测无效句柄(未在 hash_map 中找到)比检测无效指针更容易(除非它存储在 hash_map 本身中,在这种情况下我们支付查找成本,并且仍然冒着无效的指针间接的风险)。你关于类型安全的观点非常好,我完全同意。但是我并没有完全删除 RAII;在我返回它们之前,我仍然在内部存储这些对象。
    • “在我这边我负责清理,但是如果他们持有指针的时间超过了他们应该做的事情,我该怎么办?”当你创建一个对象,删除它,然后尝试在删除的指针上调用一个函数时会发生什么。即:崩溃。我根本不认为有必要为了捕获这种错误而花费这么多时间。毕竟,即使你抓住了它,你会怎么做呢?您最多可以打印一个比 SegFault/GPF 更好的错误。但是你仍然需要断言或类似的东西;你不能只是继续程序,好像一切都很好。
    • 是的,没有办法解决这个问题,这是最明智的做法。
    猜你喜欢
    • 2010-09-13
    • 2014-11-30
    • 1970-01-01
    • 1970-01-01
    • 2016-04-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-09
    相关资源
    最近更新 更多