使用函数指针的模板化双重调度

templated double-dispatching using function pointers

本文关键字:调度 函数 指针      更新时间:2023-10-16

我正在尝试为学术目的制作一个自定义碰撞引擎,但我陷入了一个一般的 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 中,它可以编译并且似乎可以正常工作......

是什么让这个演员阵容出了可怕的错误?

我真的不明白转换函数指针意味着什么......

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

可以将

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

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 维所有不同碰撞检查的数组,每对几何体一个,并且您需要类型擦除才能操作通用collidable

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

答:是的,如果 get_hit_function(( 确实兼容。

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

是的,标准明确允许从bool (collidable_object<geometry_type>::*)() constbool (collidable::*)() const static_cast,因为collidable是一个可访问的、明确的非虚拟基类collidable_object<geometry_type>

将指向成员的指针转换为相反方向(从派生到基(时,不需要static_cast。这是因为存在从bool (collidable::*)() constbool (collidable_object<geometry_type>::*)() const的有效"标准转换"。

[会议记忆]

类型为"指向 cv T 类型的 B 成员的指针"的右值,其中 B 是类

类型,可以转换为"指向 cv T 类型的 D 成员的指针"类型的右值,其中 D 是 B 的派生类。如果 B 是 D 的不可访问、不明确或虚拟基类,则需要此转换的程序格式不正确。转换的结果引用与转换发生之前指向成员的指针相同的成员,但它引用基类成员,就好像它是派生类的成员一样。结果引用了 D 实例中的 B 中的成员。

由于存在此有效的标准转换,并且collidable是可访问的明确非虚拟基类collidable_object<geometry_type>因此可以使用static_cast从基转换为派生。

[expr.static.cast]

"指向 cv1 T 类型的 D 成员的指针"类型的 rvalue,如果存在从"指向 T 类型为 B 的成员的指针"到"指向 T 类型为 D 的成员的指针"类型的 rvalue,则 B 是 D 的基类的有效标准转换, CV2 与 CV1 具有相同的 CV 资格,或比 CV1 更高的 CV 资格。[...]如果类 B 包含原始成员,或者是包含原始成员的类的基类或派生类,则指向成员的结果指针指向原始成员。否则,转换的结果是未定义的。[...]

通过指向基成员的指针调用派生类

成员函数时,必须确保用于调用它的基类对象是派生类的实例。否则,未定义的行为!

这是一个工作示例。取消注释 main 的最后一行会展示未定义的行为 - 不幸的是,这会破坏查看器。

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

关于后续问题,实现双重调度的规范方法是使用 Visitor 模式。下面是如何将此应用于方案的工作示例:

#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);
}