使用函数指针进行模板化双调度

时间:2013-07-05 00:00:53

标签: c++ templates c++11 function-pointers double-dispatch

我正在尝试为学术目的制作一个自定义碰撞引擎,而我却陷入了一般的c ++编程问题。我已经拥有了所有正常工作的几何形状,并且碰撞测试也正常工作。

引擎使用这两个类来创建要测试的几何队列:

class collidable;

template<typename geometry_type>
class collidable_object : public collidable;

由于可能有多种几何类型,我不想手动指定任何要测试的碰撞。

相反,我使用这种“技术”来实现双重调度:

class collidable
{
public:
    typedef bool (collidable::*collidable_hit_function)(const collidable& ) const;

    virtual ~collidable() = 0 {}

    virtual collidable_hit_function get_hit_function() const = 0;
};

template<typename geometry_type>
class collidable_object : public collidable
{
public:
    explicit collidable_object( geometry_type& geometry ) :
        m_geometry( geometry )
    {}

    ~collidable_object(){}

    virtual collidable_hit_function get_hit_function() const
{
    return static_cast<collidable_hit_function>( &collidable_object<geometry_type>::hit_function<geometry_type> );
}

template<typename rhs_geometry_type>
bool hit_function( const collidable& rhs ) const
{
    return check_object_collision<geometry_type, rhs_geometry_type>( *this, rhs );
}

const geometry_type& geometry() const
{
    return m_geometry;
}

private:
    geometry_type& m_geometry;
};

bool check_collision( const collidable& lhs, const collidable& rhs )
{
    collidable::collidable_hit_function hit_func = lhs.get_hit_function();

    return (lhs.*hit_func)( rhs );
}

其中函数check_object_collision是一个模板函数,用于测试碰撞并且已经过测试。

我的问题如下:函数get_hit_function中的强制转换确实编译但看起来很可疑......我做了一些可怕的错误会导致未定义的行为和多个噩梦或是否可以转换模板成员函数指针从一个派生类到另一个派生类。

令我困惑的是,在visual c ++ 2012中,这个编译并且似乎才能正常工作......

什么能让这个演员出错呢?

我真的不明白什么是转换函数指针...

作为后续问题,是否有办法以安全的方式实现这一点

3 个答案:

答案 0 :(得分:1)

可以将指针从基类转换为派生类。在相反的方向,这是非常糟糕的主意。想想如果有人会像你这样使用你的代码会发生什么:

collidable_object<A> a;
collidable_hit_function f = a.get_hit_function();

collidable_object<B> b;
b.*f(...);

Yuor hit_function(由f指出)预计thiscollidable_object<A>,但是它会collidable_object<B>。如果这两个类足够相似,那么你就不会得到错误,但是你的代码可能已经做了一些其他事情。如果你真的需要,你可以像这样使用它,但是你必须注意你只在正确的类上使用这个指针。

然而,更重要的是,你所做的事情很可能在概念上是错误的。如果您有两种几何类型AB,并检查是否与

发生碰撞
collidable_object<A> a;
collidable_object<B> b;
check_collision(a,b);

那你所做的最终是打电话:

check_object_collision<A, A>();

所以你要检查碰撞,好像两个collidable都是几何A - 我猜这不是你想要做的。

这个问题可能无法解决任何单一语言结构,因为它需要2维全部不同的碰撞检查数组,每对几何结构一个并且您需要类型擦除才能操作泛型{ {1}}秒。

答案 1 :(得分:0)

问:将模板成员函数指针从一个派生类转换为另一个派生类是否可以?

答:是的,如果get_hit_function()真正兼容。

如果这是Java或C#,我只是声明一个接口:)

答案 2 :(得分:0)

是的,标准明确允许static_castbool (collidable_object<geometry_type>::*)() const bool (collidable::*)() const,因为collidablecollidable_object<geometry_type>可访问的明确的非虚拟基类static_cast }。

当转换指向成员的方向相反时 - 从派生到基础 - bool (collidable::*)() const不是必需的。这是因为从bool (collidable_object<geometry_type>::*)() constcollidable存在有效的“标准转化”。

  

[conv.mem]

     

类型“指向cv T类型B的成员的指针”的rvalue,其中B是类类型,可以转换为类型为“指向cv T类型的D的成员的指针”的右值,其中D是a B的派生类。如果B是D的不可访问,模糊或虚拟基类,则需要这种转换的程序是不正确的。转换的结果引用与转换发生前指向成员的指针相同的成员,但它引用基类成员,就好像它是派生类的成员一样。结果是指D的实例中的成员。[...]

由于存在此有效标准转换,并且collidable_object<geometry_type>static_cast的可访问明确的非虚拟基类,因此可以使用#include <iostream> struct Geom { virtual void accept(Geom& visitor) = 0; virtual void visit(struct GeomA&) = 0; virtual void visit(struct GeomB&) = 0; }; struct GeomA : Geom { void accept(Geom& visitor) { visitor.visit(*this); } void visit(GeomA& a) { std::cout << "a -> a" << std::endl; } void visit(GeomB& b) { std::cout << "a -> b" << std::endl; } }; struct GeomB : Geom { void accept(Geom& visitor) { visitor.visit(*this); } void visit(GeomA& a) { std::cout << "b -> a" << std::endl; } void visit(GeomB& b) { std::cout << "b -> b" << std::endl; } }; void collide(Geom& l, Geom& r) { l.accept(r); } int main() { GeomA a; GeomB b; collide(a, a); collide(a, b); collide(b, a); collide(b, b); } 从base转换为派生。< / p>

  

[expr.static.cast]

     

类型“指向Cv1 T类型D的成员的指针”的rvalue可以转换为类型为“指向cv2 T类型B的成员的指针”的rvalue,其中B是D的基类,如果有效存在从“指向类型T的B的成员的指针”到“指向类型T的D的成员的指针”的标准转换,并且cv2与cv1具有相同的cv-限定条件或更高的cv-限定条件。 [...]如果类B包含原始成员,或者是包含原始成员的类的基类或派生类,则指向成员的结果指针指向原始成员。否则,演员的结果是不确定的。 [...]

当通过指向成员的指针调用派生类成员函数时,必须确保调用它的基类对象是derived-class的实例。否则,未定义的行为!

这是一个working example。取消注释main的最后一行表明了未定义的行为 - 不幸的是它打破了观众。

编译器做了什么工作?这是一个完全不同的问题;)。

关于后续问题,实现双重调度的规范方法是使用访问者模式。以下是working example如何将其应用于您的方案:

{{1}}