0

I’m trying to make a custom collision engine for academic purposes and I got stuck on a general c++ programming issue. I already have all the geometries which work properly and the collision test also working properly.

The engine uses those 2 classes to create a queue of geometries to test :

class collidable;

template<typename geometry_type>
class collidable_object : public collidable;

Since multiple geometry types are possible, I didn't want to have to specify manually any collisions to be tested.

Instead I used this "technique" to implement the double-dispatching:

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

where the function check_object_collision is a template function which tests for collision and has been tested.

My question is as follows: the cast in the function get_hit_function does compile but seems suspicious... am I doing something horribly wrong which will lead to undefined behavior and multiple nightmares or is it OK to cast template member function pointers from one derived class to another.

what confuses me is that in visual c++ 2012 this compiles and seems to work properly...

What could make this cast go horribly wrong?

I don't really understand what casting function pointers implies...

As a follow up question, would there be a way to implement this in a safe way

tiridactil
  • 389
  • 1
  • 11
  • Don't rely on compile with msvc and templated code to tell you too much. Write some tests and pass types into this to confirm compilation if you are using msvc (in particular). We have had similar issues. – dirvine Jul 05 '13 at 01:08
  • @dirvine I have run some tests but it seems like the kind of situation where a problem could easily stay hidden a long time before causing visible symptoms and I don't want to pass a week debugging this in a year... that's why I don – tiridactil Jul 05 '13 at 01:12
  • How is this double dispatch? It appears to single dispatch on `lhs`, no different than if you had a `virtual do_collide` method. – Yakk - Adam Nevraumont Jul 05 '13 at 01:39
  • @Yakk it is not single dispatching since once you execute the hit function, the "goal" was to execute `check_object_collision` with both geometry types, the one from `this` and the one from `rhs` – tiridactil Jul 05 '13 at 01:55
  • C++ does not care what your goal is. The code above does no double dispatch, and the design does not seem to allow for it. The design does single dispatch, and it might as well be a direct `virtual` method: the complexity you added is not doing anything. – Yakk - Adam Nevraumont Jul 05 '13 at 02:03
  • @Yakk then the follow-up question was how do I achieve double-dispatch since the `virtual` method won't work (since template virtual function don't compile) – tiridactil Jul 05 '13 at 02:06

3 Answers3

1

It is OK to cast pointer to a method from base class to derived class. In opposite direction it's very bad idea. Think what will happen if somebody will use your code like this:

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

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

Yuor hit_function (pointed to by f) will expect this to be collidable_object<A>, but insted it will get collidable_object<B>. If those two classes are similar enough you will not get errors, but your code is probably already doing something else than it should. You can cas it like that if you really have to, but then you must take care that youuse this pointer only on the right class.

More importantly however, what you are doing is most likely conceptually wrong. If you have two geometry types A and B, and you check for collision with

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

then what you do is eventually call:

check_object_collision<A, A>();

so you are checking for collision as if both collidables were of geometry A - I am guessing it's not what you want to do.

This is problem that you will probably not solve with any single language construct, as it requires 2-dimensionall array of different collision-checks, one for each pair of geometry AND you need type-erasure to be able to manipulate generic collidables.

j_kubik
  • 6,062
  • 1
  • 23
  • 42
0

Q: Is it OK to cast template member function pointers from one derived class to another?

A: Yes, if get_hit_function() is truly compatible.

If this were Java or C#, I'd just declare an interface :)

paulsm4
  • 114,292
  • 17
  • 138
  • 190
  • could you define "truly compatible". Also since you can't create a virtual template method I don't know how to implement the interface... – tiridactil Jul 05 '13 at 00:09
0

Yes, static_cast from bool (collidable_object<geometry_type>::*)() const to bool (collidable::*)() const is explicitly allowed by the standard, because collidable is an accessible unambiguous non-virtual base-class of collidable_object<geometry_type>.

When converting a pointer-to-member in the opposite direction - from derived to base - static_cast is not required. This is because there exists a valid 'standard conversion' from bool (collidable::*)() const to bool (collidable_object<geometry_type>::*)() const.

[conv.mem]

An rvalue of type “pointer to member of B of type cv T,” where B is a class type, can be converted to an rvalue of type “pointer to member of D of type cv T,” where D is a derived class of B. If B is an inaccessible, ambiguous or virtual base class of D, a program that necessitates this conversion is ill-formed. The result of the conversion refers to the same member as the pointer to member before the conversion took place, but it refers to the base class member as if it were a member of the derived class. The result refers to the member in D’s instance of B. [...]

Because this valid standard conversion exists, and collidable is an accessible unambiguous non-virtual base-class of collidable_object<geometry_type> it is possible to use static_cast to convert from base to derived.

[expr.static.cast]

An rvalue of type “pointer to member of D of type cv1 T” can be converted to an rvalue of type “pointer to member of B of type cv2 T”, where B is a base class of D, if a valid standard conversion from “pointer to member of B of type T” to “pointer to member of D of type T” exists, and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. [...] If class B contains the original member, or is a base or derived class of the class containing the original member, the resulting pointer to member points to the original member. Otherwise, the result of the cast is undefined. [...]

When you call a derived-class member-function through a pointer-to-member-of-base, you must ensure that the base-class object you call it with is an instance of the derived-class. Otherwise, undefined behaviour!

Here's a working example. Uncommenting the last line of main demonstrates the undefined behaviour - which unfortunately breaks the viewer.

What does the compiler do under the hood to make this work? That's a whole different question ;).

Regarding the follow up question, the canonical way to implement double-dispatch is using the Visitor pattern. Here's a working example of how this could be applied to your scenario:

#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);
}
Community
  • 1
  • 1
willj
  • 2,991
  • 12
  • 24
  • (-1) I have a C++ standard here (C++98) and mentioned citation says exactly the opposite - that pointer-to-base-field can be converted to pointer-to-derived-field and not otherwise. Conversion you suggest possible would easily lead to problems as I explained in my answer. – j_kubik Jul 07 '13 at 01:39
  • edited to elaborate. The scenario in question seems to me to be one where this type of cast is genuinely useful, providing care is taken to avoid the undefined behaviour. – willj Jul 07 '13 at 10:06
  • "When you call a derived-class member-function through a pointer-to-member-of-base, you must ensure that the base-class object you call it with is an instance of the derived-class. Otherwise, undefined behaviour!" - Yes, exactly. But to that you need to simply static (or dynamic) cast your base-pointer to class-instance to derived-pointer to class-instance, and not casting the pointer-to-member. – j_kubik Jul 07 '13 at 23:25
  • [expr.static.cast] that you cite mentions that cast will only produce defined behavior if pointer points to the member that is already in B, or in it's base class(es). That means that casting in line 34 of your example will already produce undefined behavior, no matter how you use the pointer afterwards - usually it will work simply because most pointers are similar, but there is no guarantee that it will. I can give examples of circumstances in which it will probably fail. – j_kubik Jul 07 '13 at 23:33
  • http://stackoverflow.com/questions/60000/c-inheritance-and-member-function-pointers – willj Jul 08 '13 at 08:22