没有 RTTI 的 C++ 双分派“可扩展”

Posted

技术标签:

【中文标题】没有 RTTI 的 C++ 双分派“可扩展”【英文标题】:C++ double dispatch "extensible" without RTTI 【发布时间】:2011-09-14 18:21:34 【问题描述】:

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

【问题讨论】:

编译时双重分派是否适合您? (我猜可能不会,但以防万一,有一个简单的解决方案。) 不是,因为它不允许可扩展性(我正在尝试编写一个库,而不仅仅是一组预定义的类)。 @Konrad,出于好奇,您在考虑 CRTP 吗?如果是这样,@George 是否有它不可行的原因? 因为我需要动态多态性。否则,它本来是完全可行的。 您希望不使用 RTTI 的什么?由于我们在谈论动态调度,所以 RTTI 的要求。如果您想要最小化手动dynamic_casts 的数量(即避免在所有级别的每个潜在参数类型都必须有一个dynamic_cast,那可能是可行的,但如果您想避免dynamic_cast不惜一切代价,那么你需要语言的支持,然后答案很简单:你不能。 【参考方案1】:

首先要意识到双(或更高阶)调度无法扩展。带单 dispatch 和 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 on 指向对象的指针),并手动管理插入顺序以定义重载 你希望的分辨率。但是,使用n^2 函数,这可能会很快变慢。自从 有排序,应该可以用std::map,但是排序函数要 实施起来绝对不是微不足道的(并且仍然必须在整个过程中使用dynamic_cast 地点)。

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

【讨论】:

@James:既然OP提到了without RTTI,恐怕基于typeid的解决方案不适合。 @Matthieu 我以为他想分发类型。在这种情况下,没有 RTTI 是矛盾的:你不能在运行时根据类型调度而不在运行时确定类型。 @James:除了 RTTI,还有其他方法可以“了解”类型。例如,LLVM 创建了一个相当智能(并且重要的是选择加入)的替代方案。 基本上,由于编译器支持不佳,并且每个人都说“在嵌入式环境中不需要 RTTI”,我希望 RTTI 退出谈判桌。但是,指示类型的变量是一个可行的选择。毕竟,vtable 本质上是以某种方式做到这一点的。 @James:看来我们用同一个词来表示不同的目的。对我来说,RTTI 特别涉及运行时类型信息的 C++ 标准实现(用于typeiddynamic_cast)。请注意,在我介绍的 LLVM 示例中,较弱形式的 RTTI 以不同的方式支持使用 isa&lt;&gt;cast&lt;&gt;。您只知道是否可以转换,而不是完整的运行时信息。【参考方案2】:

C++ 中的"visitor pattern" 通常等同于双重分派。它不使用 RTTI 或 dynamic_casts。

另请参阅this question 的答案。

【讨论】:

好的,但是访问者必须知道整个类层次结构,这也是不可接受的。就没有别的办法了吗? 另外,访问者模式必须只访问类层次结构中的叶子。 @George Penn:它可以访问其他离开的东西。 @Matthieu 是的,我刚刚检查过了。我的错。 @George Penn:你说得对,默认情况下,Visitor 将转到重载accept 方法的最派生类(并且访问者具有visit 重载)。不过,您可以完美地将Child::accept(v) 实现为 v.visit(*this); Parent::visit(*this); 【参考方案3】:

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

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

【讨论】:

【参考方案4】:

您可能想检查 LLVM 如何将 isa&lt;&gt;dyn_cast&lt;&gt;cast&lt;&gt; 实现为模板系统,因为它是在没有 RTTI 的情况下编译的。

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

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

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

【讨论】:

【参考方案5】:

您可以通过自己实现多次调度的编译时逻辑来伪造行为。然而,这非常乏味。 Bjarne Stroustrup has co-authored a paper 描述了如何在编译器中实现这一点

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

collision(foo, bar);

至少

一样复杂
DynamicDispatchTable::lookup(collision_signature, FooClass, BarClass)(foo, bar);

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

【讨论】:

谢谢,这是一个答案,但是詹姆斯的答案更详细,所以我接受他的。

以上是关于没有 RTTI 的 C++ 双分派“可扩展”的主要内容,如果未能解决你的问题,请参考以下文章

Java中的重载和多分派

设计模式---访问者模式

设计模式之观察者模式与访问者模式详解和应用

什么是task

有没有办法配置 Alamofire 不将结果分派到主队列?

今日拾遗 20200627:java 的动态分派,到底包含哪些知识点?