C++双调度"extensible",无需 RTTI
C++ double dispatch "extensible" without RTTI
有谁知道一种方法可以在不使用 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。这是假设所有方法都只接受两个参数。一旦需要更多参数(即使这些参数不是多重调度的一部分(,这就会变得更加复杂,并且需要规避类型安全。
- 理解boost::asio-async_read在无需读取内容时的行为
- dynamic_cast without RTTI
- CMake "--target install"无需"--build"命令行
- 专用于 std 元组的模板,而无需用户执行remove_cvref
- 使用基类中的派生方法运行线程,而无需使用模板
- 使用 RTTI 克隆唯一指针的向量
- 按字母顺序对字符串中的字母进行排序,而无需使用内置的 sort()
- 将 boost::odeint 与向量类一起使用,而无需调整向量的大小
- 是否可以使用其他变量为变量分配值,而无需在 C++ 中更改其值?
- 如何声明由多个线程调用的 C++ DLL 的内部类,而无需导出类
- 将有状态的 lambda 传递到 C 样式函数中,而无需上下文参数
- 如何在逗号后使用 cout 打印整数值,而无需在逗号后添加额外的零C++?
- 将字符串转换为浮点数或整数,而无需使用内置函数(如 atoi 或 atof)
- OpenMP:for 循环避免数据竞争,而无需使用关键
- VkSurfaceKHR 指针的值在函数调用后更改,无需任何显式赋值
- 从函数中全局删除并重新实例化数组结构,而无需在编译时知道数组的大小
- 将字符数组复制到 QByte 数组中,而无需传递指针
- 在C++中将字符串转换为双精度,而无需科学记数法
- 使用记事本C++打开 txt 文件时无需等待
- C++双调度"extensible",无需 RTTI