C++双调度"extensible",无需 RTTI

C++ double dispatch "extensible" without RTTI

本文关键字:无需 RTTI extensible 调度 C++      更新时间:2023-10-16

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

首先要意识到的是,双阶(或更高阶(调度无法扩展。 带单调度和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 );

(你显然会想把它包装成一个函数;这不是你想要分散的那种东西。整个代码。

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

如果你想100%正确地做到这一点,一些函数能够处理一个中间类及其所有衍生物,然后您需要某种更动态的搜索,并订购控制过载分辨率。 例如,考虑:

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

如果你有f(I1, D2a)f(D1a, I2),应该选择哪一个。 最简单的解决方案只是一个线性搜索,选择第一个可以调用的(由dynamic_cast确定(指向对象的指针(,并手动管理插入顺序以定义重载您想要的分辨率。 但是,使用n^2函数时,这可能会很快变慢。 因为有一个排序,应该可以使用std::map,但排序功能将绝对不平凡地实现(并且仍然dynamic_cast必须在整个地方(。

考虑到所有因素,我的建议是将双重调度限制在小型封闭层次结构中,并坚持访客模式的某种变体。

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

另请参阅此问题的答案。

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

第二个问题是一个大问题。如何创建可与可扩展类层次结构很好地缩放的双重调度机制。这很难。在编译时(静态多态性(,这可以通过函数重载(和/或模板专用化(很好地完成。在运行时,这要困难得多。据我所知,正如 Konrad 所提到的,唯一的解决方案是保留函数指针(或类似性质的东西(的调度表。在我看来,通过使用静态多态性并将调度函数拆分为类别(如函数签名和其他东西(,您可以避免违反类型安全。但是,在实现这一点之前,你应该非常认真地考虑你的设计,看看这个双重调度是否真的有必要,它是否真的需要是一个运行时调度,以及它是否真的需要为所涉及的两个类的每个组合都有一个单独的函数(也许你可以想出一个减少和固定数量的抽象类来捕获你需要实现的所有真正不同的方法(。

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

它有点麻烦(所涉及的每个类都需要大量代码(,但非常轻量级。

LLVM程序员手册有一个很好的例子和对实现的引用。

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

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

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

collision(foo, bar);

至少

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

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