2

コールバック機能へのアクセスを提供するためのインターフェイスをいくつか作成しています。つまり、インターフェイス Aから継承すると、クラスはタイプ 1 のコールバックを使用できます。インターフェイス Bはタイプ 2 を許可します。A と B の両方から継承すると、両方のタイプのコールバックが可能になります。最終的な目的は、クラス A と B がそれらから継承するだけですべての汚れた作業を処理することです。

最初の問題

これは、私が抱えている問題のいくつかを示す小さな例です。

class A
{
public:
    static void AFoo( void* inst )
    {
        ((A*)inst)->ABar( );
    }
    virtual void ABar( void ) = 0;
};

class B
{
public:
    static void BFoo( void* inst )
    {
        ((B*)inst)->BBar( );
    }
    virtual void BBar( void ) = 0;
};

class C : public A, public B
{
public:
    void ABar( void ){ cout << "A"; };
    void BBar( void ){ cout << "B"; };
};

そして電話をかけることで

C* c_inst = new C( );
void (*AFoo) (void*) = C::AFoo;
void (*BFoo) (void*) = C::BFoo;
AFoo( (void*)c_inst );
BFoo( (void*)c_inst );

出力として「AB」が得られると思います。代わりに「AA」を取得します。派生クラスの順序を逆にすると (A の前に B)、「BB」が生成されます。どうしてこれなの?

2番目の問題

私が使用している実際のインターフェイスはテンプレート化されているため、コードは次のように見えます

template <class T> class A
{
public:
    static void AFoo( void* inst )
    {
        ((T*)inst)->ABar( );
    }
    virtual void ABar( void ) = 0;
};

template <class T> class B
{
public:
    static void BFoo( void* inst )
    {
        ((T*)inst)->BBar( );
    }
    virtual void BBar( void ) = 0;
};

class C : public A<C>, public B<C>
{
public:
    void ABar( void ){ cout << "A"; };
    void BBar( void ){ cout << "B"; };
};

この理由は、A と B がすべての作業を実行できるようにするためですが、それらの実装には C の知識は必要ありません。

今、

C* c_inst = new C( );
void (*AFoo) (void*) = C::AFoo;
void (*BFoo) (void*) = C::BFoo;
AFoo( (void*)c_inst );
BFoo( (void*)c_inst );

正しい出力「AB」を生成します。

この小さな例はここでは問題なく動作しますが、実際には常に正しく動作するとは限りません。上記の最初の問題の奇妙さと同様に、非常に奇妙なことが起こり始めます。主な問題は、両方の仮想関数 (または静的関数など) が常に C に変換されるとは限らないことです。

たとえば、C::AFoo() を正常に呼び出すことはできますが、常に C::BFoo() を呼び出すとは限りません。これは、A と B から派生する順序に依存するclass C: public A<C>, public B<C>場合があります。AFoo と BFoo のどちらも機能しないコードを生成することもあれば、どちらか一方または両方が機能class C: public B<C>, public A<C>するコードを生成することもあります。

クラスはテンプレート化されているので、A と B の仮想関数を削除できます。そうすることで、もちろん C に ABar と BBar が存在する限り、機能するコードが生成されます。それは許容できますが、望ましくありません。むしろ何が問題なのか知りたいです。

上記のコードが奇妙な問題を引き起こす可能性がある理由として、どのようなことが考えられますか?

最初の例では正しくないのに、2 番目の例では正しい出力が生成されるのはなぜですか?

4

2 に答える 2

5

未定義の動作を呼び出しています。X*anを aにキャストすることはできvoid*ますが、一度それを行うと、安全にキャストできるvoid*のは anだけX*です (これは完全に真実というわけではありません。単純化しすぎていますが、議論のためにそうであると仮定します)。

では、なぜコードはそのままのように動作するのでしょうか? MI を実装する 1 つの方法は、次のようなものです。

 struct A
 {
    A_vtable* vtbl;
 };

 struct B
 {
    B_vtable* vtbl;
 };

 struct C
 {
    struct A;
    struct B;
 };

この例では A が最初ですが、順序はコンパイラによって決定されます。void にキャストすると、C の先頭へのポインターが得られます。その void* をキャストして戻すと、必要に応じてポインターを適切に調整するために必要な情報が失われます。A と B の両方に同じシグネチャを持つ単一の仮想関数があるため、impl を呼び出すことになります。オブジェクト レイアウトでたまたま最初にあるクラスの。

于 2012-02-02T02:41:35.040 に答える
1

Logan Capaldo が語ったように、コールバック実装のこのアプローチには問題があります。void* を XXX* にキャストすることは安全ではありません。キャストされたポインターが実際に XXX を指していることを保証できなかったからです (他の型や無効なアドレスを指している可能性があり、予期しない問題が発生する可能性があります)。私の提案は、静的関数の引数の型をインターフェイスの型に変更することです。つまり、次のとおりです。

class A
{
public:
    static void AFoo( A* inst )
    {
        inst->ABar( );
    }
    virtual void ABar( void ) = 0;
};

class B
{
public:
    static void BFoo( B* inst )
    {
         inst->BBar( );
    }
    virtual void BBar( void ) = 0;
};

class C : public A, public B
{
public:
    void ABar( void ){ cout << "A"; };
    void BBar( void ){ cout << "B"; };
};


C* c_inst = new C( );
void (*AFoo) (void*) = C::AFoo;
void (*BFoo) (void*) = C::BFoo;
AFoo(c_inst );
BFoo(c_inst );

これらは利点です: まず void* キャストは必要ありません。次に、ユーザーに正しいポインター型を渡すように強制します。また、ユーザーはドキュメントなしでこれらの静的関数の正確な引数の型を知っています。

于 2012-02-02T03:53:33.630 に答える