【问题标题】:templated double-dispatching using function pointers使用函数指针的模板化双重调度
【发布时间】:2013-07-05 00:00:52
【问题描述】:

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

什么会让这个演员出现可怕的错误?

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

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

【问题讨论】:

  • 不要依赖使用 msvc 和模板代码编译来告诉你太多。如果您正在使用 msvc(特别是),请编写一些测试并将类型传递到其中以确认编译。我们也遇到过类似的问题。
  • @dirvine 我已经运行了一些测试,但它似乎是一种问题很容易在导致明显症状之前隐藏很长时间的情况,我不想通过一周的时间来调试它一年...这就是为什么我不想依赖经验测试
  • 这个双重派送怎么样?它似乎是在lhs 上单次调度,与您使用virtual do_collide 方法没有什么不同。
  • @Yakk 它不是单一调度,因为一旦你执行了 hit 函数,“目标”就是使用两种几何类型执行 check_object_collision,一种来自 this,另一种来自 @987654329 @
  • C++ 不在乎你的目标是什么。上面的代码没有双重分派,而且设计似乎不允许这样做。设计做单次分派,还不如直接virtual 方法:你添加的复杂性没有做任何事情。

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


【解决方案1】:

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

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

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

您的hit_function(由f 指向)将期望thiscollidable_object&lt;A&gt;,但它会得到collidable_object&lt;B&gt;。如果这两个类足够相似,您将不会出错,但您的代码可能已经在做一些其他事情。如果你真的需要的话,你可以这样设置它,但是你必须注意你只在正确的类上使用这个指针。

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

collidable_object<A> a;
collidable_object<B> b;
check_collision(a,b);

那么你所做的就是最终调用:

check_object_collision<A, A>();

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

这是您可能无法使用任何单一语言构造解决的问题,因为它需要不同碰撞检查的二维数组,每对几何图形一个,并且您需要类型擦除才能操作通用 @ 987654333@s.

【讨论】:

  • 我已经考虑过二维数组(或地图或任何容器)解决方案,但缺陷(这对我来说似乎很重要)是我不知道如何在不明确指定每个几何图形的情况下初始化所述数组手。由于我使用的大多数几何图形本身都是模板,因此在我看来,类型列表会变得很大,以至于无法维护......
  • 您希望编译器如何自动知道如何检查几何类型 A 和 B 之间的冲突? check_object_collision&lt;A, B&gt;() 是否明确专门用于任何两种组合?如果是这样,您真的别无选择,只能手动构建所述数组 - C++ 对您没有多大帮助。您确定使用模板指定几何图形是最好的方法吗?
【解决方案2】:

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

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

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

【讨论】:

  • 你能定义“真正兼容”吗?另外由于你不能创建一个虚拟模板方法我不知道如何实现接口......
【解决方案3】:

是的,标准明确允许从bool (collidable_object&lt;geometry_type&gt;::*)() constbool (collidable::*)() conststatic_cast,因为collidablecollidable_object&lt;geometry_type&gt; 的可访问的明确非虚拟基类。

当以相反方向转换指向成员的指针时 - 从派生到基 - 不需要static_cast。这是因为存在从bool (collidable::*)() constbool (collidable_object&lt;geometry_type&gt;::*)() const 的有效“标准转换”。

[conv.mem]

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

因为存在这种有效的标准转换,并且collidablecollidable_object&lt;geometry_type&gt; 的一个可访问的明确非虚拟基类,所以可以使用static_cast 从基类转换为派生类。

[expr.static.cast]

“指向 cv1 T 的 D 成员的指针”类型的右值可以转换为“指向 cv2 T 类型的 B 成员的指针”类型的右值,其中 B 是 D 的基类,如果有效存在从“指向类型 T 的 B 成员的指针”到“指向类型 T 的 D 成员的指针”的标准转换,并且 cv2 与 cv1 具有相同的 cv 限定或大于 cv1 的 cv 限定。 [...] 如果类 B 包含原始成员,或者是包含原始成员的类的基类或派生类,则指向成员的结果指针指向原始成员。否则,强制转换的结果是未定义的。 [...]

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

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

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

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

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

【讨论】:

  • (-1) 我在这里有一个 C++ 标准 (C++98),并且提到的引用正好相反——指向基域的指针可以转换为派生的指针——场,而不是其他。正如我在回答中解释的那样,您建议的转换很容易导致问题。
  • 编辑详细。在我看来,有问题的场景是这种类型的演员阵容真正有用的场景,需要注意避免未定义的行为。
  • "当你通过指向基成员的指针调用派生类成员函数时,你必须确保你调用它的基类对象是派生类的实例-class。否则,未定义的行为! - 对,就是这样。但为此,您需要简单地静态(或动态)将基指针转换为类实例,将派生指针转换为类实例,而不是将指针转换为成员。
  • 您引用的 [expr.static.cast] 提到,如果指针指向已经在 B 中或其基类中的成员,则强制转换只会产生定义的行为。这意味着在您的示例的第 34 行进行转换已经产生了未定义的行为,无论您之后如何使用指针 - 通常它会工作,因为大多数指针都是相似的,但不能保证它会。我可以举例说明它可能会失败的情况。
猜你喜欢
  • 1970-01-01
  • 2017-07-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-18
  • 1970-01-01
相关资源
最近更新 更多