6

有谁知道在 C++ 中使用 RTTI 和 dynamic_cast<> 正确处理双重调度的方法,还有一个解决方案,其中类层次结构是可扩展的,即基类可以进一步派生,其定义/实现不需要知道吗?
我怀疑没有办法,但我很高兴被证明是错误的:)

4

5 回答 5

6

C++ 中的“访问者模式”通常等同于双重调度。它不使用 RTTI 或 dynamic_casts。

另请参阅此问题的答案。

于 2011-06-14T14:37:34.883 回答
6

首先要意识到的是双(或更高阶)调度无法扩展。使用单一调度和n类型,您需要n函数;对于双重调度n^2,等等。你如何处理这个问题部分决定了你如何处理双重调度。一个明显的解决方案是通过创建一个封闭的层次结构来限制派生类型的数量;在这种情况下,可以使用访问者模式的变体轻松实现双重调度。如果您不关闭层次结构,那么您有几种可能的方法。

如果你坚持每一对都对应一个函数,那么你基本上需要一个:

std::map<std::pair<std::type_index, std::type_index>, void (*)(Base const& lhs, Base const& rhs)>
                dispatchMap;

(根据需要调整函数签名。)您还必须实现这些n^2函数,并将它们插入到dispatchMap. (我在这里假设您使用自由函数;没有合乎逻辑的理由将它们放在其中一个类中而不是另一个类中。)之后,您调用:

(*dispatchMap[std::make_pair( std::type_index( typeid( obj1 ) ), std::type_index( typeid( obj2 ) )])( obj1, obj2 );

(您显然希望将其包装到一个函数中;这不是您希望分散在代码中的那种东西。)

一个小的变体是说只有某些组合是合法的。在这种情况下,您可以使用 , 并finddispatchMap找不到所需内容时生成错误。(预计会有很多错误。)如果您可以定义某种默认行为,则可以使用相同的解决方案。

如果你想 100% 正确地做到这一点,其中一些函数能够处理中间类及其所有派生类,那么你需要某种更动态的搜索和排序来控制重载决议。考虑例如:

            Base
         /       \
        /         \
       I1          I2
      /  \        /  \
     /    \      /    \
    D1a   D1b   D2a   D2b

如果你有一个f(I1, D2a)和一个f(D1a, I2),应该选择哪一个。最简单的解决方案只是线性搜索,选择第一个可以调用的(由dynamic_cast指向对象的指针确定),并手动管理插入顺序以定义您希望的重载解决方案。但是,对于n^2函数,这可能会很快变慢。既然有排序,应该可以使用std::map,但是排序函数的实现肯定不是微不足道的(并且仍然必须在dynamic_cast所有地方使用)。

考虑到所有因素,我的建议是将双重分派限制在小的、封闭的层次结构中,并坚持访问者模式的某些变体。

于 2011-06-14T17:38:38.267 回答
3

第一个问题是微不足道的。dynamic_cast涉及两件事:运行时检查和类型转换。前者需要RTTI,后者不需要。用不需要 RTTI 的功能来替换 dynamic_cast 所需要做的就是拥有自己的方法来在运行时检查类型。为此,您所需要的只是一个简单的虚函数,它返回某种类型的标识或它遵循的更具体的接口(可以是枚举、整数 ID,甚至是字符串)。对于static_cast强制转换,一旦您自己完成了运行时检查,并且您确定要强制转换的类型在对象的层次结构中,您就可以安全地执行 a。因此,这解决了模拟“完整”功能的问题dynamic_cast无需内置 RTTI。另一个更复杂的解决方案是创建自己的 RTTI 系统(就像在几个软件中完成的那样,比如 Matthieu 提到的 LLVM)。

第二个问题是一个大问题。如何创建一个可扩展的类层次结构的双重调度机制。这很难。在编译时(静态多态性),这可以通过函数重载(和/或模板特化)很好地完成。在运行时,这要困难得多。据我所知,Konrad 提到的唯一解决方案是保留函数指针(或类似性质的东西)的调度表。在我看来,通过使用静态多态性并将分派函数分成类别(如函数签名和东西),您可以避免违反类型安全。但是,在实现这个之前,你应该仔细考虑你的设计,看看这个双重调度是否真的有必要,如果它真的需要一个运行时调度,

于 2011-06-14T17:10:04.817 回答
2

您可能想检查 LLVM 如何实现isa<>以及作为模板系统,因为它是在没有 RTTI 的情况下编译的dyn_cast<>cast<>

它有点麻烦(需要涉及的每个类中的代码花絮)但非常轻量级。

LLVM Programmer's Manual有一个很好的例子和实现的参考。

(所有 3 种方法共享相同的代码花絮)

于 2011-06-14T15:27:38.773 回答
2

您可以通过自己实现多次调度的编译时逻辑来伪造行为。然而,这是非常乏味的。Bjarne Stroustrup 与人合着了一篇论文,描述了如何在编译器中实现这一点。

底层机制——调度表——可以动态生成。然而,使用这种方法你当然会失去所有的语法支持。您需要维护方法指针的二维矩阵,并根据参数类型手动查找正确的方法。这将呈现一个简单的(假设的)调用

collision(foo, bar);

至少

DynamicDispatchTable::lookup(collision_signature, FooClass, BarClass)(foo, bar);

因为你不想使用 RTTI。这是假设你所有的方法都只接受两个参数。一旦需要更多参数(即使它们不是多重分派的一部分),这仍然会变得更加复杂,并且需要规避类型安全。

于 2011-06-14T16:11:45.780 回答