C++双重调度“可扩展”无 RTTI

发布于 2024-11-15 09:21:59 字数 148 浏览 3 评论 0原文

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

Does anyone know a way to have double dispatch handled correctly in C++ without using RTTI and dynamic_cast<> and also a solution, in which the class hierarchy is extensible, that is the base class can be derived from further and its definition/implementation does not need to know about that?
I suspect there is no way, but I'd be glad to be proven wrong :)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

自此以后,行同陌路 2024-11-22 09:21:59

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

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

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

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

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

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

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

The first thing to realize is that double (or higher order) dispatch doesn't scale. With single
dispatch, and n types, you need n functions; for double dispatch n^2, and so on. How you
handle this problem partially determines how you handle double dispatch. One obvious solution is to
limit the number of derived types, by creating a closed hierarchy; in that case, double dispatch can
be implemented easily using a variant of the visitor pattern. If you don't close the hierarchy,
then you have several possible approaches.

If you insist that every pair corresponds to a function, then you basically need a:

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

(Adjust the function signature as necessary.) You also have to implement the n^2 functions, and
insert them into the dispatchMap. (I'm assuming here that you use free functions; there's no
logical reason to put them in one of the classes rather than the other.) After that, you call:

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

(You'll obviously want to wrap that into a function; it's not the sort of thing you want scattered
all over the code.)

A minor variant would be to say that only certain combinations are legal. In this case, you can use
find on the dispatchMap, and generate an error if you don't find what you're looking for.
(Expect a lot of errors.) The same solution could e used if you can define some sort of default
behavior.

If you want to do it 100% correctly, with some of the functions able to handle an intermediate class
and all of its derivatives, you then need some sort of more dynamic searching, and ordering to
control overload resolution. Consider for example:

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

If you have an f(I1, D2a) and an f(D1a, I2), which one should be chosen. The simplest solution
is just a linear search, selecting the first which can be called (as determined by dynamic_cast on
pointers to the objects), and manually managing the order of insertion to define the overload
resolution you wish. With n^2 functions, this could become slow fairly quickly, however. Since
there is an ordering, it should be possible to use std::map, but the ordering function is going to
be decidedly non-trivial to implement (and will still have to use dynamic_cast all over the
place).

All things considered, my suggestion would be to limit double dispatch to small, closed hierarchies,
and stick to some variant of the visitor pattern.

〃温暖了心ぐ 2024-11-22 09:21:59

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

另请参阅此问题的答案。

The "visitor pattern" in C++ is often equated with double dispatch. It uses no RTTI or dynamic_casts.

See also the answers to this question.

世界等同你 2024-11-22 09:21:59

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

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

The first problem is trivial. dynamic_cast involves two things: run-time check and a type cast. The former requires RTTI, the latter does not. All you need to do to replace dynamic_cast with a functionality that does the same without requiring RTTI is to have your own method to check the type at run-time. To do this, all you need is a simple virtual function that returns some sort of identification of what type it is or what more-specific interface it complies to (that can be an enum, an integer ID, even a string). For the cast, you can safely do a static_cast once you have already done the run-time check yourself and you are sure that the type you are casting to is in the object's hierarchy. So, that solves the problem of emulating the "full" functionality of dynamic_cast without needing the built-in RTTI. Another, more involved solution is to create your own RTTI system (like it is done in several softwares, like LLVM that Matthieu mentioned).

The second problem is a big one. How to create a double dispatch mechanism that scales well with an extensible class hierarchy. That's hard. At compile-time (static polymorphism), this can be done quite nicely with function overloads (and/or template specializations). At run-time, this is much harder. As far as I know, the only solution, as mentioned by Konrad, is to keep a dispatch table of function pointers (or something of that nature). With some use of static polymorphism and splitting dispatch functions into categories (like function signatures and stuff), you can avoid having to violate type safety, in my opinion. But, before implementing this, you should think very hard about your design to see if this double dispatch is really necessary, if it really needs to be a run-time dispatch, and if it really needs to have a separate function for each combination of two classes involved (maybe you can come up with a reduced and fixed number of abstract classes that capture all the truly distinct methods you need to implement).

相对绾红妆 2024-11-22 09:21:59

您可能想检查 LLVM 如何将 isa<>dyn_cast<>cast<> 实现为模板系统,因为它是编译时不使用 RTTI。

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

LLVM 程序员手册 有一个很好的示例和实现参考。

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

You may want to check how LLVM implement isa<>, dyn_cast<> and cast<> as a template system, since it's compiled without RTTI.

It is a bit cumbersome (requires tidbits of code in every class involved) but very lightweight.

LLVM Programmer's Manual has a nice example and a reference to the implementation.

(All 3 methods share the same tidbit of code)

绾颜 2024-11-22 09:21:59

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

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

collision(foo, bar);

至少变得像

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

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

You can fake the behaviour by implementing the compile-time logic of multiple dispatch yourself. However, this is extremely tedious. Bjarne Stroustrup has co-authored a paper describing how this could be implemented in a compiler.

The underlying mechanism – a dispatch table – could be dynamically generated. However, using this approach you would of course lose all syntactical support. You’d need to to maintain 2-dimensional matrix of method pointers and manually look up the correct method depending on the argument types. This would render a simple (hypothetical) call

collision(foo, bar);

at least as complicated as

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

since you didn’t want to use RTTI. And this is assuming that all your methods take only two arguments. As soon as more arguments are required (even if those aren’t part of the multiple dispatch) this becomes more complicated still, and would require circumventing type safety.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文