为了这个问题,这是大大简化的。说我有一个层次结构:
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
具有较大Precision
中的任何一个类型相同;像下面的伪代码:
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的答案很好。有什么想法吗?
答案 0 :(得分:4)
你所要求的是multiple dispatch,又称多方法。它不是C ++语言的一个特性。
有特殊情况的解决方法,但你不能避免自己做一些实现。
多次调度的一种常见模式称为“redispatch”,又称“递归延迟调度”。基本上,一个虚方法解析一个参数类型然后调用另一个虚方法,直到所有参数都被解析。外部的功能(如果有的话)只调用这些虚拟方法中的第一个。
假设有n个派生类,将有一种方法可以解析第一个参数,n会解析第二个参数,n * n来解析第三个,依此类推 - 最糟糕的是,无论如何。这是相当多的手工工作,并且使用基于typeid的条件块可能更容易进行初始开发,但是使用redispatch进行维护时更加健壮。
class Base;
class Derived1;
class Derived2;
class Base
{
public:
virtual void Handle (Base* p2);
virtual void Handle (Derived1* p1);
virtual void Handle (Derived2* p1);
};
class Derived1 : public Base
{
public:
void Handle (Base* p2);
void Handle (Derived1* p1);
void Handle (Derived2* p1);
};
void Derived1::Handle (Base* p2)
{
p2->Handle (this);
}
void Derived1::Handle (Derived1* p1)
{
// p1 is Derived1*, this (p2) is Derived1*
}
void Derived1::Handle (Derived2* p1)
{
// p1 is Derived2*, this (p2) is Derived1*
}
// etc
使用派生类的模板实现这一点很困难,并且处理它的模板元编程可能是不可读的,不可维护的并且非常脆弱。使用非模板方法实现调度,然后使用mixin模板(将其基类作为模板参数的模板类)来扩展具有附加功能的模板可能并不是那么糟糕。
visitor design pattern与(主要使用)redispatch IIRC密切相关。
另一种方法是使用一种用于处理问题的语言,并且有一些选项适用于C ++。一种是使用treecc - 一种特定于域的语言来处理AST节点和多调度操作,像lex和yacc一样,生成“源代码”作为输出。
处理调度决策的所有工作都是基于AST节点ID生成switch语句 - 这可以很容易地成为动态类型的值类ID IYSWIM。但是,这些是您不必编写或维护的switch语句,这是一个关键的区别。我遇到的最大问题是AST节点的析构函数处理被篡改,这意味着除非您特别努力,否则不会调用成员数据的析构函数 - 即它最适合字段的POD类型。
另一种选择是使用支持多方法的语言预处理器。其中有一些,部分原因是因为Stroustrup在某一方面确实有很好的支持多方法的想法。 CMM就是一个。 Doublecpp是另一个。另一个是Frost Project。我相信CMM最接近Stroustrup所描述的,但我没有检查过。
但最终,多次调度只是做出运行时决策的一种方式,并且有很多方法可以处理相同的决策。专业的DSL带来了相当多的麻烦,所以你通常只在你需要大量的多次发送时才这样做。 Redispatch和访问者模式是强大的WRT维护,但是以牺牲一些复杂性和混乱为代价。对于简单的情况,简单的条件语句可能是更好的选择,但要注意在编译时检测未处理的情况的可能性是很困难的,如果不是不可能的话。
通常情况下,没有一种正确的方法可以做到这一点,至少在C ++中是这样。
答案 1 :(得分:3)
你不能直接将函数()委托给模板化代码而不在所有可能类型的大量列表之间进行选择,因为模板在编译时被扩展,而在编译时函数()不知道派生类型是什么它实际上会被调用。您需要为模板化的operation
函数的每个版本编译模板化实例,这可能是一个无限集。
遵循该逻辑,唯一知道可能需要的所有模板的地方是Derived
类本身。因此,您的Derived
类应包含成员:
Derived<Precision> *operation(Base& arg2) {
Derived<Precision> *ptr = new Derived<Precision>;
// ...
return ptr;
}
然后,您可以像这样定义function
,并间接进行调度:
Base* function(const Base& a, const Base& b) {
if (a.precision() > b.precision())
return a.operation(b);
else
return b.operation(a);
}
请注意,这是简化版;如果你的操作在其参数中不是对称的,你需要定义两个版本的成员函数 - 一个用this
代替第一个参数,一个用它代替第二个参数。
此外,这忽略了这样一个事实:a.operation
需要某种方式才能获得b.value
的适当形式,而不知道b
的派生类型。你必须自己解决这个问题 - 注意它(通过与之前相同的逻辑)不可能通过模板化b
的类型来解决这个问题,因为你在运行时调度。解决方案取决于您所具有的确切类型,以及是否有某种方法可以使某种更高精度的值从等于或低于精度的Derived
对象中提取值而不知道其确切类型宾语。这可能是不可能的,在这种情况下,您已经获得了类型ID匹配的长列表。
但是,您不必在switch语句中执行此操作。您可以为每个Derived
类型提供一组成员函数,以便向上转换为更高精度的函数。例如:
template<int i>
upCast<Derived<i> >() {
return /* upcasted value of this */
}
然后,您的operator
成员函数可以在b.upcast<typeof(this)>
上运行,并且不必显式执行转换以获取所需类型的值。您可能必须显式实例化其中一些函数才能对它们进行编译;我没有做足够的工作与RTTI肯定地说。
但从根本上说,问题在于,如果你有N个可能的精度,你就有N N个可能的组合,而且每个组合实际上都需要有单独编译的代码。如果你不能在function
的定义中使用模板,那么你必须拥有这些可能性的所有N N的编译版本,并且你必须告诉编译器全部生成它们,并且不知何故,你必须选择正确的一个在运行时调度。使用成员函数的技巧取出了N中的一个因子,但另一个仍然存在,并且没有办法使它完全通用。
答案 2 :(得分:1)
首先,您希望使precision
成员成为static const int
值,而不是函数,以便您可以在编译时对其进行操作。在Derived
中,它将是:
static const int precision = Precision;
然后,您需要一些辅助结构来确定最精确的Base / Derived类。首先,您需要一个通用的helper-helper结构来根据布尔值选择两种类型之一:
template<typename T1, typename T2, bool use_first>
struct pickType {
typedef T2 type;
};
template<typename T1, typename T2>
struct pickType<T1, T2, true> {
typedef T1 type;
};
然后,如果pickType<T1, T2, use_first>::type
为T1
,则use_first
将解析为true
,否则将解析为T2
。那么,我们使用它来选择最精确的类型:
template<typename T1, typename T2>
struct mostPrecise{
typedef pickType<T1, T2, (T1::precision > T2::precision)>::type type;
};
现在,mostPrecise<T1, T2>::type
将为您提供两种类型中较大precision
值的任何一种。因此,您可以将您的函数定义为:
template<typename T1, typename T2>
mostPrecise<T1, T2>::type function(const T1& a, const T2& b) {
// ...
}
你有它。