我正在创建一些旨在提供对回调功能的访问的接口。也就是说,继承自接口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”。这是为什么?
我正在使用的实际接口是模板化的,因此代码看起来更像是
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。这是可以接受的,但不是所希望的;我宁愿知道问题所在。
上述代码可能导致奇怪问题的原因有哪些?
为什么第二个例子产生正确的输出,尽管第一个没有?
答案 0 :(得分:5)
您正在调用未定义的行为。您可以将X*
投射到void*
,但是一旦完成,void*
投放X*
的唯一安全就是 struct A
{
A_vtable* vtbl;
};
struct B
{
B_vtable* vtbl;
};
struct C
{
struct A;
struct B;
};
(这是{{1}}不完全正确,我过于简单化,但为了争论,假装它是。)
现在为什么代码表现得像?实现MI的一种方法类似于:
{{1}}
在此示例中,A是第一个,但顺序将由编译器确定。当你转换为void时,你会得到一个指向C开头的指针。当你向后转换那个void *时,你已经失去了在必要时适当调整指针所需的信息。由于A和B都有一个具有相同签名的虚函数,因此最终调用impl。无论哪个类在对象布局中首先出现。
答案 1 :(得分: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 );
这些是好处:首先不需要虚空*演员。其次,它强制用户传递正确的指针类型。并且用户在没有任何文档的情况下知道这些静态函数的确切参数类型。