0

学術目的でカスタム衝突エンジンを作成しようとしていますが、一般的な C++ プログラミングの問題に行き詰まりました。適切に機能するすべてのジオメトリが既にあり、衝突テストも適切に機能しています。

エンジンはこれら 2 つのクラスを使用して、テストするジオメトリのキューを作成します。

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でこれがコンパイルされ、適切に動作しているように見えることです...

このキャストがひどく間違っている原因は何ですか?

関数ポインターのキャストが何を意味するのかよくわかりません...

フォローアップの質問として、これを安全な方法で実装する方法はありますか

4

3 に答える 3

1

メソッドへのポインターを基底クラスから派生クラスにキャストしても問題ありません。反対方向では、それは非常に悪い考えです。誰かがあなたのコードを次のように使用するとどうなるか考えてみてください:

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

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

Yuor hit_function( によって指されているf) は になると予想thisされますがcollidable_object<A>、実際には になりますcollidable_object<B>。これらの 2 つのクラスが十分に類似している場合、エラーは発生しませんが、コードはおそらく、本来の目的以外のことを既に実行しています。本当に必要な場合は、そのようにキャストできますが、このポインターは正しいクラスでのみ使用するように注意する必要があります。

ただし、さらに重要なことは、あなたがしていることは概念的に間違っている可能性が最も高いということです。と の 2 つのジオメトリ タイプがAありB、 との衝突をチェックする場合

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

あなたがすることは、最終的に呼び出すことです:

check_object_collision<A, A>();

collidableそのため、両方がジオメトリであるかのように衝突をチェックしていますA-それはあなたがやりたいことではないと思います。

これは、ジオメトリのペアごとに 1 つずつ、異なる衝突チェックの 2 次元配列が必要であり、ジェネリックを操作できるようにするために型消去が必要なため、おそらく単一の言語構造では解決できない問題ですcollidable

于 2013-07-05T01:39:28.713 に答える
0

Q: ある派生クラスから別の派生クラスにテンプレート メンバー関数ポインターをキャストしても問題ありませんか?

A: はい、get_hit_function() が本当に互換性がある場合。

これが Java または C# の場合は、インターフェイスを宣言するだけです :)

于 2013-07-05T00:05:47.853 に答える
0

はい、static_castfrom bool (collidable_object<geometry_type>::*)() consttobool (collidable::*)() constは標準で明示的に許可されています。これcollidableは、 のアクセス可能な明確な非仮想基本クラスであるためcollidable_object<geometry_type>です。

ポインターからメンバーへの反対方向 (派生からベース) への変換static_castは必要ありません。bool (collidable::*)() constこれは、 からへの有効な「標準変換」が存在するためですbool (collidable_object<geometry_type>::*)() const

[コンバージョンメモリ]

「cv T 型の B のメンバーへのポインター」型の右辺値 (B はクラス型) は、「cv T 型の D のメンバーへのポインター」型の右辺値に変換できます。ここで、D はの派生クラスです。 B. B が D のアクセスできない、あいまいな、または仮想基底クラスである場合、この変換を必要とするプログラムは形式が正しくありません。変換の結果は、変換前のメンバーへのポインターと同じメンバーを参照しますが、基底クラスのメンバーを派生クラスのメンバーであるかのように参照します。結果は、B の D のインスタンスのメンバーを参照します [...]

この有効な標準変換が存在し、collidableアクセス可能な明確な非仮想基本クラスであるため、基本から派生への変換collidable_object<geometry_type>に使用できます。static_cast

[expr.static.cast]

「cv1 T 型の D のメンバーへのポインター」型の右辺値は、「cv2 T 型の B のメンバーへのポインター」型の右辺値に変換できます。ここで、B は D の基底クラスです。 「タイプ T の B のメンバへのポインタ」から「タイプ T の D のメンバへのポインタ」が存在し、cv2 が cv1 と同じ cv 修飾、またはそれより大きい cv 修飾です。[...] クラス B が元のメンバーを含むか、元のメンバーを含むクラスの基本クラスまたは派生クラスである場合、メンバーへの結果のポインターは元のメンバーを指します。それ以外の場合、キャストの結果は未定義です。[...]

基本メンバーへのポインターを介して派生クラスのメンバー関数を呼び出す場合は、それを呼び出す基本クラス オブジェクトが派生クラスのインスタンスであることを確認する必要があります。そうでなければ、未定義の動作です!

これが実際のです。main の最後の行のコメントを外すと、未定義の動作が示されます - 残念ながらビューアーが壊れます。

これを機能させるために、コンパイラは内部で何をしますか? それはまったく別の質問です;)。

フォローアップの質問に関して、二重ディスパッチを実装する標準的な方法は、訪問者パターンを使用することです。これをシナリオに適用する方法の実例を次に示します。

#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);
}
于 2013-07-06T15:03:17.463 に答える