2010-03-12 39 views
5

为了解决这个问题,这非常简化。说我有一个层次:基于模板参数在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); 

凡函数的结果的具体类型是相同类型取其ab具有较大的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); 

} 

AB是特定类型的分别ab。我想要function独立于有多少个实例Derived进行操作。我想避免比较大规模的typeid()表,虽然RTTI很好的答案。有任何想法吗?

+2

我想你应该提到你不知道完整的类的类型。你只知道'Base&'。包括我自己在内的几个答案确实假设你知道确切类型'Derived '。 – 2010-03-12 22:15:11

+0

注意对一个答案的评论:另一个要求是该函数不能是一个模板;它必须具有给定的Base *(Base&Base)签名。 – 2010-03-12 22:20:12

+0

将限制更清楚地纳入问题中。 – 2010-03-12 22:23:26

回答

3

由于模板在编译时展开,并且在编译时函数()不知道派生函数(),所以不能在函数()委托给模板化代码,而无需在所有可能类型的大量列表之间进行选择它将被实际调用的类型。您需要编译每个版本的模板化代码的模板化代码实例,这些代码将是必需的,这可能是无限集合。

遵循该逻辑,知道所有可能需要的模板的唯一地方就是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的情况下获得适当形式的b.value。您必须自己解决这个问题 - 请注意,它是(通过与之前相同的逻辑)不可能通过b类型的模板来解决此问题,因为您在运行时调度。解决方案完全取决于您获得的类型,以及是否有某种方式可以获得更高的精度,以便在不知道该对象的确切类型的情况下将对象的值从等于或低于对象的精度中提取出来。这可能是不可能的,在这种情况下,你有很长的类型ID匹配。

不过你不必在switch语句中这样做。您可以将每个Derived类型的一组成员函数上传到更高精度的函数中。例如:

template<int i> 
upCast<Derived<i> >() { 
    return /* upcasted value of this */ 
} 

然后,你operator成员函数可以在b.upcast<typeof(this)>操作,不会有明确做铸造得到它所需要的类型的值。你可能不得不明确地实例化这些函数中的一些来编译它们;我没有和RTTI做足够的工作来确定地说。

问题是,如果你有N个可能的精度,你有N个可能的组合,而且其中每个组合都需要单独编译代码。如果你不能在你的function的定义中使用模板,那么你必须编译所有这些可能性的N版本,并且不知何故你必须告诉编译器生成它们,不知何故你必须选择正确的一个在运行时调度。使用成员函数的诀窍除去了N的其中一个因素,但另一个仍然存在,并且无法使其成为完全通用的。

+0

这对我来说已经够用了。第二个因素是因为'Base'定义了一个用于在运行时转换为任意类型的工具。 – 2010-03-12 22:47:55

+0

哦,好!我只是在编辑,建议这样的设施对于消除第二个因素是有用的。 – 2010-03-12 22:49:50

1

首先,您想让您的precision成员的值为static const int,而不是函数,以便您可以在编译时对其进行操作。在Derived,这将是:

static const int precision = Precision; 

然后,你需要一些辅助结构,以确定最精确的底座/派生类。首先,你需要一个通用的帮手,帮手结构挑取决于布尔两种类型之一:

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_firsttrue,否则到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) { 
    // ... 
} 

而你就有它。

+0

这对我的另一个项目非常有趣和有用,但我特别需要在运行时作出决定。 – 2010-03-12 22:16:01

+0

谢谢。我会留下这个历史价值,而不是删除它,然后,看看我的其他答案,在运行时做这个评论。 – 2010-03-12 22:37:25

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。但是,这些是您不必编写或维护的开关语句,这是一个关键区别。我遇到的最大问题是AST节点的摧毁处理器被篡改,这意味着除非您做出特别的努力,否则将不会调用成员数据的析构函数,即它最适合用于字段的POD类型。

另一种选择是使用支持多方法的语言预处理器。其中有一些,部分原因是Stroustrup确实有相当完善的想法支持多方法。 CMM就是其中之一。另一个是Doublecpp。另一个是Frost Project。我相信CMM与Stroustrup描述的最接近,但我没有检查。

然而,最终,多派遣只是一种做出运行时决策的方式,并且有很多方法可以处理相同的决策。专业的DSL带来了相当多的麻烦,所以你通常只会在需要大量多次调度的情况下才能做到这一点。 Redispatch和访问者模式都是强大的WRT维护,但是以一些复杂性和混乱为代价。对于简单的情况,简单的条件语句可能是更好的选择,但是要注意,在编译时检测未处理的情况的可能性如果不是不可能的话,则很难。

往往是这样,至少在C++中没有一个正确的方法来做到这一点。