2

我正在创建几个旨在提供对回调功能的访问的接口。也就是说,从接口 A继承允许类使用类型一的回调;接口 B允许类型二。从 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”。颠倒派生类的顺序(B 在 A 之前),产生“BB”。为什么是这样?

第二个问题

我使用的实际接口是模板化的,所以代码看起来更像

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。这是可以接受的,但不是我们想要的;我宁愿知道问题是什么。

上述代码可能导致奇怪问题的原因有哪些?

为什么第二个示例会产生正确的输出,而第一个示例却没有?

4

2 回答 2

5

您正在调用未定义的行为。您可以将 anX*转换为 a void*,但是一旦您这样做了,唯一安全的void*就是转换为 an X*(这并不完全正确,我过于简单化了,但为了论证起见假装它是)。

现在为什么代码表现得像现在这样?实现 MI 的一种方法类似于:

 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 回答