在 C++ 中模拟动态调度基于模板参数
为了解决这个问题,这被大大简化了。假设我有一个层次结构:
struct Base {
virtual int precision() const = 0;
};
template<int Precision>
struct Derived : public Base {
typedef Traits<Precision>::Type Type;
Derived(Type data) : value(data) {}
virtual int precision() const { return Precision; }
Type value;
};
我想要一个带有签名的非模板函数:
Base* function(const Base& a, const Base& b);
其中函数结果的特定类型与a
和b
具有更高的精度
;类似下面的伪代码:
Base* function(const Base& a, const Base& b) {
if (a.precision() > b.precision())
return new A( ((A&)a).value + A(b.value).value );
else if (a.precision() < b.precision())
return new B( B(((A&)a).value).value + ((B&)b).value );
else
return new A( ((A&)a).value + ((A&)b).value );
}
其中 A
和 B
分别是 a
和 b
的特定类型。我希望 function
能够独立于有多少 Derived
实例化而运行。我想避免使用大量 typeid()
比较表,尽管 RTTI 的答案很好。有什么想法吗?
This is heavily simplified for the sake of the question. Say I have a hierarchy:
struct Base {
virtual int precision() const = 0;
};
template<int Precision>
struct Derived : public Base {
typedef Traits<Precision>::Type Type;
Derived(Type data) : value(data) {}
virtual int precision() const { return Precision; }
Type value;
};
I want a non-template function with the signature:
Base* function(const Base& a, const Base& b);
Where the specific type of the result of the function is the same type as whichever of a
and b
has the greater Precision
; something like the following pseudocode:
Base* function(const Base& a, const Base& b) {
if (a.precision() > b.precision())
return new A( ((A&)a).value + A(b.value).value );
else if (a.precision() < b.precision())
return new B( B(((A&)a).value).value + ((B&)b).value );
else
return new A( ((A&)a).value + ((A&)b).value );
}
Where A
and B
are the specific types of a
and b
, respectively. I want function
to operate independently of how many instantiations of Derived
there are. I'd like to avoid a massive table of typeid()
comparisons, though RTTI is fine in answers. Any ideas?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
您要求的是多重调度,又名多重方法。它不是 C++ 语言的功能。
对于特殊情况有一些解决方法,但您无法避免自己进行一些实现。
多重调度的一种常见模式称为“重新调度”,也称为“递归延迟调度”。基本上,一个虚拟方法解析一种参数类型,然后调用另一种虚拟方法,直到解析所有参数。外部的函数(如果有的话)只调用这些虚拟方法中的第一个。
假设有 n 个派生类,则将有一个方法来解析第一个参数,n 来解析第二个参数,n*n 来解析第三个参数,依此类推——无论如何,最坏的情况也是如此。这是相当大量的手动工作,并且使用基于 typeid 的条件块对于初始开发来说可能更容易,但使用重新调度对于维护来说更稳健。
使用派生类的模板来实现这一点将很困难,并且处理它的模板元编程可能不可读、不可维护并且非常脆弱。不过,使用非模板方法实现调度,然后使用 mixin 模板(一个将其基类作为模板参数的模板类)来扩展它的附加功能可能还不错。
访问者设计模式与redispatch IIRC密切相关(基本上使用)。
另一种方法是使用专为处理该问题而设计的语言,并且有一些选项可以很好地与 C++ 配合使用。一种是使用 treecc - 一种用于处理 AST 节点和多次调度操作的特定于域的语言,像 lex 和 yacc 一样,生成“源代码”作为输出。
它处理调度决策所做的就是根据 AST 节点 ID 生成 switch 语句 - 它可以很容易地成为动态类型值类 ID,IYSWIM。但是,这些是您不必编写或维护的 switch 语句,这是一个关键的区别。我遇到的最大问题是 AST 节点的析构函数处理被篡改,这意味着除非您付出特别的努力,否则不会调用成员数据的析构函数 - 即它最适合字段的 POD 类型。
另一种选择是使用支持多方法的语言预处理器。其中有一些,部分原因是 Stroustrup 确实有相当完善的想法来支持多种方法。 CMM 就是其中之一。 Doublecpp 是另一个。另一个是Frost 项目。我相信 CMM 最接近 Stroustrup 所描述的,但我还没有检查过。
但归根结底,多重分派只是做出运行时决策的一种方式,并且有多种方法可以处理同一决策。专业 DSL 会带来相当多的麻烦,因此通常只有在需要大量多重调度时才这样做。重新调度和访问者模式是强大的 WRT 维护,但代价是一些复杂性和混乱。对于简单的情况,简单的条件语句可能是更好的选择,但请注意,在编译时检测未处理的情况的可能性即使不是不可能,也是很困难的。
通常情况下,没有一种正确的方法可以做到这一点,至少在 C++ 中是这样。
What you are asking for is called multiple dispatch, aka multimethods. It isn't a feature of the C++ language.
There are workarounds for special cases, but you can't avoid doing some implementation yourself.
One common pattern for multiple dispatch is called "redispatch", aka "recursive deferred dispatching". Basically, one virtual method resolves one parameters type then calls another virtual method, until all parameters are resolved. The function of the outside (if there is one) just calls the first of these virtual methods.
Assuming there are n derived classes, there will be one method to resolve the first parameter, n to resolve the second, n*n to resolve the third and so on - at worst, anyway. That's a fair amount of manual work, and using conditional blocks based on typeid might be easier for initial development, but it's more robust for maintenance to use redispatch.
Implementing this using a template for the derived classes would be difficult, and the template metaprogramming to handle it would probably be unreadable, unmaintainable and very fragile. Implementing the dispatch using non-template methods, then using a mixin template (a template class that takes its base class as a template parameter) to extend that with additional features may not be so bad, though.
The visitor design pattern is closely related to (basically implemented using) redispatch IIRC.
The other approach is to use a language designed to handle the problem, and there are a few options which work well with C++. One is to use treecc - a domain-specific language for handling AST nodes and multiple-dispatch operations which, like lex and yacc, generates "source code" as output.
All it does to handle the dispatch decisions is generate switch statements based on an AST node ID - which can just as easily be a dynamically-typed value class ID, IYSWIM. However, these are switch statements that you don't have to write or maintain, which is a key difference. The biggest issue I have is that AST nodes have their destructor handling tampered with, meaning that destructors for member data don't get called unless you make a special effort - ie it works best with POD types for fields.
Another option is to use a language preprocessor that supports multimethods. There have been a few of these, partly because Stroustrup did have fairly well developed ideas for supporting multimethods at one point. CMM is one. Doublecpp is another. Yet another is the Frost Project. I believe CMM is closest to what Stroustrup described, but I haven't checked.
Ultimately, though, multiple dispatch is just a way to make a run-time decision, and there are many ways to handle the same decision. Specialist DSLs bring a fair amount of hassle, so you generally only do it if you need a lot of multiple dispatch. Redispatch and the visitor pattern are robust WRT maintenance, but at the expense of some complexity and clutter. Simple conditional statements may be a better choice for simple cases, though beware that detecting the possibility of an unhandled case at compile-time is then difficult if not impossible.
As is often the case, there is no one right way to do it, at least in C++.
如果不在所有可能类型的大量列表中进行选择,则不能直接将 function() 委托给模板化代码,因为模板在编译时扩展,并且在编译时 function() 不知道它实际上会是什么派生类型被称为与.您需要为所需的模板化
操作
函数的每个版本编译模板化代码的实例,这可能是一个无限集。按照该逻辑,唯一知道可能需要的所有模板的地方是
Derived
类本身。因此,您的Derived
类应该包含一个成员:然后,您可以像这样定义
function
,并间接进行调度:请注意,这是简化版本;如果您的操作在其参数中不对称,则需要定义成员函数的两个版本 - 一个用
this
代替第一个参数,另一个用它代替第二个参数。此外,这忽略了这样一个事实:您需要某种方式让
a.operation
获得适当形式的b.value
,而不知道b< 的派生类型。 /代码>。您必须自己解决这个问题 - 请注意,(按照与之前相同的逻辑)不可能通过在
b
类型上进行模板化来解决这个问题,因为您是在运行时调度的。解决方案取决于您所拥有的类型,以及是否有某种方法可以让更高精度的类型从等于或更低精度的Derived
对象中提取值,而无需知道确切的值该对象的类型。这可能是不可能的,在这种情况下,您会得到一长串类型 ID 的匹配列表。不过,您不必在 switch 语句中执行此操作。您可以为每个
Derived
类型提供一组成员函数,以便向上转换为更高精度的函数。例如:然后,您的
operator
成员函数可以对b.upcast
进行操作,并且不必显式进行转换来获取值它需要的类型。您可能必须显式实例化其中一些函数才能编译它们;我还没有对 RTTI 做足够的工作来确定这一点。但从根本上来说,问题在于,如果您有 N 种可能的精度,那么您就有 NN 种可能的组合,而每一种组合实际上都需要单独编译的代码。如果您不能在
function
的定义中使用模板,那么您必须拥有所有 NN 这些可能性的编译版本,并且您必须以某种方式告诉编译器生成它们所有,并且您必须以某种方式选择正确的一个来在运行时分派。使用成员函数的技巧去掉了 N 的其中一个因素,但另一个仍然存在,并且没有办法使其完全通用。You can't have function() delegate to templated code directly without selecting between a massive list of all possible types, because templates are expanded at compile-time, and at compile-time function() does not know what derived types it will actually be called with. You need to have compiled instantiations of the templated code for every version of your templated
operation
function that will be required, which is potentially an infinite set.Following that logic, the only place that knows all of the templates that might be required is the
Derived
class itself. Thus, yourDerived
class should include a member:Then, you can define
function
like so, and do the dispatching indirectly:Note that this is the simplified version; if your operation is not symmetric in its arguments, you'll need to define two versions of the member function -- one with
this
in place of the first argument, and one with it in place of the second.Also, this has ignored the fact that you need some way for
a.operation
to get an appropriate form ofb.value
without knowing the derived type ofb
. You'll have to solve that one yourself -- note that it's (by the same logic as earlier) impossible to solve this by templating on the type ofb
, because you're dispatching at runtime. The solution depends on exactly what types you've got, and whether there is some way for a type of higher precision to pull a value out of an equal-or-lower precisionDerived
object without knowing the exact type of that object. This may not be possible, in which case you've got the long list of matching on type IDs.You don't have to do that in a switch statement, though. You can give each
Derived
type a set of member functions for up-casting to a function of greater precision. For example:Then, your
operator
member function can operate onb.upcast<typeof(this)>
, and will not have to explicitly do the casting to get a value of the type it needs. You may well have to explicitly instantiate some of these functions to get them to be compiled; I haven't done enough work with RTTI to say for sure.Fundamentally, though, the issue is that if you've got N possible precisions, you've got NN possible combinations, and each of these will in fact need to have separately-compiled code. If you can't use templates in your definition of
function
, then you have to have compiled versions of all NN of these possibilities, and somehow you have to tell the compiler to generate them all, and somehow you have to pick the right one to dispatch to at runtime. The trick of using a member function takes out one of those factors of N, but the other one remains, and there's no way to make it entirely generic.首先,您希望将您的 precision 成员设置为 static const int 值,而不是函数,以便您可以在编译时对其进行操作。在
Derived
中,它将是:然后,您需要一些辅助结构来确定最精确的基类/派生类。首先,您需要一个通用的 helper-helper 结构来根据布尔值选择两种类型之一:
然后,
pickType::type
将解析为T1use_first
为true
,则返回T2
,否则返回T2
。因此,我们用它来选择最精确的类型:现在,
mostPrecise::type
将为您提供两种类型中具有更高精度
的类型价值。因此,您可以将您的函数定义为:现在您就拥有了它。
First, you want to make your
precision
member astatic const int
value, not a function, so that you can operate on it at compile time. InDerived
, it would be:Then, you need some helper structs to determine the most-precise Base/Derived class. First, you need a generic helper-helper struct to pick one of two types depending on a boolean:
Then,
pickType<T1, T2, use_first>::type
will resolve toT1
ifuse_first
istrue
, and otherwise toT2
. So, then we use this to pick the most precise type:Now,
mostPrecise<T1, T2>::type
will give you whichever of the two types has a greaterprecision
value. So, you can define your function as:And there you have it.