基于模板参数的C ++动态调度仿真

时间:2010-03-12 21:54:55

标签: c++ polymorphism rtti dynamic-dispatch

为了这个问题,这是大大简化的。说我有一个层次结构:

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的答案很好。有什么想法吗?

3 个答案:

答案 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>::typeT1,则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) {
  // ...
}

你有它。